IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    记一次瞎逼折腾

    Liu Yuyang发表于 2016-02-19 07:50:09
    love 0

    dotcloud悄无声息的远去了。我收到让转移应用的邮件才知道公司已经破产。

    就在几天前,往dotcloud上部署微信机器人时还饶有兴趣瞎折腾了一下。当时发在cnodejs上

    昨天,看到dotcloud提供一个secure shell,忽然脑洞大开,我觉得又可以花式Tunnel了。

    1
    2
    3
    4
    5
    6
    ┌─[reverland@reverland-R478-R429] - [~] - [2016-01-23 11:55:29]
    └─[0] <> export PATH=$PATH:$HOME/.local/bin
    ┌─[reverland@reverland-R478-R429] - [~] - [2016-01-23 11:55:34]
    └─[0] <> dcapp wechat/default run bash
    Connecting...
    [wechat/default]:~$

    好奇,我能不用dcapp直接连接吗?什么原理?

    于是翻了翻下载到dotcloudng的源码,开源软件就是好啊就是好。看到里头有个ssh命令

    cmd = ssh_cmd(host_name, 'delete-cache', deployment_name)
    

    于是打印了一下,发现就是普通的ssh连接。于是抱着试试看的心理连了一次,还真可以。。

    ssh -t -p 2222 -- wechat-default@sshforwarder.dotcloudapp.com TOKEN=t9Nd9ECasAgSD9UsYfcFwgysAF4bCL bash
    

    翻翻源码没什么问题。但是,--是啥?,token又是啥?

    --很快查到,为了防止bash解析后面的内容。但token呢?

    env = 'TOKEN={token}'.format(token=self.api.get_token()['token'])
    

    搜索了下没找到,cctrl引用了cclib,看到get_token

    1
    def get_token(self):
        """
            We use get_token to get the token.
        """
        return self._token

    那又是哪里设置了token呢?一眼看到上面的set\_token。。。

    1
    2
    3
    4
    5
    def set_token(self, token):
    """
    We use set_token to set the token.
    """

    self._token = token

    检查set_token是在api init的时候

    看看Api类,就抱着试试看的心理用试验了下。。。然而401?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var getSshToken = new Promise((resolve, reject) => {
    var req = https.request({
    method: 'POST',
    path: '/token/',
    hostname: 'api.dotcloudapp.com',
    headers: {
    'User-Agent': 'pycclib/1.6.2',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-length': 0,
    }
    }, (res)=> {
    resolve(res);
    }
    });

    req.end();
    })

    我在想要不要把mitmproxy打开看看呢。忽然在文件中赫然看到个DEBUG标志,于是打开,清晰看到几次请求。发现第一次请求是不带任何参数的,就是401,在header中返回了一个sshtoken。紧接着第二次请求。这次http header中Authorization中多了一些东西:

    ccssh signature=rqsolg/L43mTokqnwVCgfGpCxxxxxxxxxxxxxvsdv6HxXiyXkmEAg6kKvOHSjFhCprq2AuDQbU2Z7DHUcryu9bVRmBQvNOd2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/92uN4C6aUkXqCmlp16G0VC2qqE/QrEuvO72OXeMC8tL4RrU3Qn7tRzablDo2sNaCXkXMcjMtqM+DpuzqbOHZnn7lEwynbCPOtRGaGYnVRQtxxxxxxxxxxxufi6oxomKGk/6ch8C7yjEE9hfbbqFcXBZQw==,fingerprint=c6:xx:92:8a:86:xx:6b:af:fe:xx:19:62:1b:xx:2b:f0,sshtoken=unBVe7F36pCfVhtZEmPCaT,email=xxx@linuxer.me
    

    虽然公钥和数据签名也没什么影响,还是打上码= =

    sshtoken是第一步在response header中www-authenticate中给的,其他的呢。

    email很显然。。fingerprint,我自己的太熟悉了。。signature是啥?

    看了下源码,发现是这个函数生成的signature

    1
    signature = sign_token(key_path, fingerprint, sshtoken)

    数据签名函数简化如下,把错误处理去掉了,还懒得管缩进。

    1
    def sign_token(key_path, fingerprint, data):
        # from agent
        pkey = get_key_from_agent(fingerprint)
            # paramiko is inconsistent here in that the agent's key
            # returns Message objects for 'sign_ssh_data' whereas RSAKey
            # objects returns byte strings.
            # Workaround: cast both return values to string and build a
            # new Message object
            s = str(pkey.sign_ssh_data(data))
            m = Message(s)
            m.rewind()
           m.get_string() # == 'ssh-rsa':
            return base64.b64encode(m.get_string())

    于是自己查看文档试验了一下,

    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
    ┌─[reverland@reverland-R478-R429] - [~/tmp/dcwall] - [2016-01-23 01:05:51]
    └─[0] <> ssh -v -t -p 2222 -- wechat-default@sshforwarder.dotcloudapp.com TOKEN=tv8NczygRPK6cgp78azgXyKKrX9KPN bash
    OpenSSH_6.6.1, OpenSSL 1.0.1f 6 Jan 2014
    debug1: Reading configuration data /home/reverland/.ssh/config
    debug1: Reading configuration data /etc/ssh/ssh_config
    debug1: /etc/ssh/ssh_config line 19: Applying options for *
    debug1: Connecting to sshforwarder.dotcloudapp.com [130.211.165.15] port 2222.
    debug1: fd 3 clearing O_NONBLOCK
    debug1: Connection established.
    debug1: identity file /home/reverland/.ssh/id_rsa type 1
    debug1: identity file /home/reverland/.ssh/id_rsa-cert type -1
    debug1: identity file /home/reverland/.ssh/id_dsa type 2
    debug1: identity file /home/reverland/.ssh/id_dsa-cert type -1
    debug1: identity file /home/reverland/.ssh/id_ecdsa type -1
    debug1: identity file /home/reverland/.ssh/id_ecdsa-cert type -1
    debug1: identity file /home/reverland/.ssh/id_ed25519 type -1
    debug1: identity file /home/reverland/.ssh/id_ed25519-cert type -1
    debug1: Enabling compatibility mode for protocol 2.0
    debug1: Local version string SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.4
    debug1: Remote protocol version 2.0, remote software version Twisted
    debug1: no match: Twisted
    debug1: SSH2_MSG_KEXINIT sent
    debug1: SSH2_MSG_KEXINIT received
    debug1: kex: server->client aes128-ctr hmac-md5 none
    debug1: kex: client->server aes128-ctr hmac-md5 none
    debug1: SSH2_MSG_KEX_DH_GEX_REQUEST(1024<3072<8192) sent
    debug1: expecting SSH2_MSG_KEX_DH_GEX_GROUP
    debug1: SSH2_MSG_KEX_DH_GEX_INIT sent
    debug1: expecting SSH2_MSG_KEX_DH_GEX_REPLY
    debug1: Server host key: RSA 5a:83:13:7c:d7:a1:cb:7c:ec:29:99:91:e4:bc:9d:01
    debug1: Host '[sshforwarder.dotcloudapp.com]:2222' is known and matches the RSA host key.
    debug1: Found key in /home/reverland/.ssh/known_hosts:3689
    debug1: ssh_rsa_verify: signature correct
    debug1: SSH2_MSG_NEWKEYS sent
    debug1: expecting SSH2_MSG_NEWKEYS
    debug1: SSH2_MSG_NEWKEYS received
    debug1: SSH2_MSG_SERVICE_REQUEST sent
    debug1: SSH2_MSG_SERVICE_ACCEPT received
    debug1: Authentications that can continue: publickey
    debug1: Next authentication method: publickey
    debug1: Offering DSA public key: /home/reverland/.ssh/id_dsa
    debug1: Authentications that can continue: publickey
    debug1: Offering RSA public key: /home/reverland/.ssh/id_rsa
    debug1: Server accepts key: pkalg ssh-rsa blen 279
    debug1: Authentication succeeded (publickey).
    Authenticated to sshforwarder.dotcloudapp.com ([130.211.165.15]:2222).
    debug1: channel 0: new [client-session]
    debug1: Entering interactive session.
    debug1: Sending environment.
    debug1: Sending env LC_IDENTIFICATION = zh_CN.UTF-8
    debug1: Sending env LC_TIME = zh_CN.UTF-8
    debug1: Sending env LC_NUMERIC = zh_CN.UTF-8
    debug1: Sending env LC_PAPER = zh_CN.UTF-8
    debug1: Sending env LC_MEASUREMENT = zh_CN.UTF-8
    debug1: Sending env LC_ADDRESS = zh_CN.UTF-8
    debug1: Sending env LC_MONETARY = zh_CN.UTF-8
    debug1: Sending env LANG = en_US.UTF-8
    debug1: Sending env LC_NAME = zh_CN.UTF-8
    debug1: Sending env LC_TELEPHONE = zh_CN.UTF-8
    debug1: Sending env LC_CTYPE = en_US.UTF-8
    debug1: Sending command: TOKEN=tv8NczygRPK6cgp78azgXyKKrX9KPN bash
    Connecting...
    [wechat/default]:~$

    想了想这个认证过程。

    • https请求服务器,得到sshtoken
    • 用私钥给sshtoken的sha1哈希签名,连同公钥fingerprint,email,sshtoken一并发送给服务器
    • (这一步是我猜的)服务器验证fingerprint身份(之前dcuser时应该已经密码认证传过公钥,待验证),服务器使用客户公钥解密签名,将解密得到的哈希和sshtoken的sha1哈希进行对比,实现身份验证和sshtoken 验证。返回再下一步ssh连接时要传递的token
    • 客户端ssh连接forward.dotcloudapp.com,认证通过后,服务器端需要检查Token的值来启动程序实例。为什么要检查呢?我猜,因为dotcloud免费用户控制只能运行一个Worker实例。。。

    于是,自己实现了下这个过程。

    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
    102
    103
    104
    105
    106
    107
    108
    109
    var exec = require('child_process').exec;

    var https = require('https');
    var EMAIL = 'sa@linuxer.me';

    var getSshToken = new Promise((resolve, reject) => {
    var req = https.request({
    method: 'POST',
    path: '/token/',
    hostname: 'api.dotcloudapp.com',
    headers: {
    'User-Agent': 'pycclib/1.6.2',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-length': 0,
    }
    }, (res)=> {
    if ('www-authenticate' in res.headers) {
    //console.log(res.headers['www-authenticate']);
    var result = /sshtoken=(.+)$/mg.exec(res.headers['www-authenticate'])
    if (!result) {
    reject("fail to get ssh token");
    }
    var sshtoken = result[1];
    resolve(sshtoken);
    }
    });

    req.end();
    })

    function getAuth(sshtoken) {
    var p1 = getSignature(sshtoken);
    var p2 = getFingerPrint();
    var p3 = Promise.all([p1, p2]).then((k)=>{
    return new Promise((resolve, reject)=>{
    var authorization = 'ccssh ';
    authorization += ('signature=' + k[0] + ',');
    authorization += ('fingerprint=' + k[1] + ',');
    authorization += ('sshtoken=' + sshtoken + ',');
    authorization += ('email=' + EMAIL);
    console.log(authorization);
    resolve(authorization);
    });
    });
    return p3;
    }

    function getSignature(sshtoken) {
    return new Promise((resolve, reject)=>{
    var cmd = 'echo -ne "' + sshtoken + '" | openssl sha1 -binary | openssl pkeyutl -sign -inkey ~/.ssh/id_rsa -pkeyopt digest:sha1';
    //console.log(cmd);
    exec(cmd,
    // 以下两个参数非常重要
    {
    encoding: 'binary',
    shell: '/bin/bash',
    },
    (error, stdout, stderr) => {
    if (error) {
    reject(error);
    }
    // 注意要binary而不是utf8
    resolve(new Buffer(stdout, 'binary').toString('base64'));
    });
    });
    }

    function getFingerPrint() {
    return new Promise((resolve, reject)=>{
    exec('ssh-keygen -lf ~/.ssh/id_rsa.pub', (error, stdout, stderr) => {
    if (error) {
    reject(error);
    }
    resolve(stdout.toString().split(' ')[1]);
    });
    });
    }

    function getToken(authorization){
    var p = new Promise((resolve, reject)=>{
    var req = https.request({
    method: 'POST',
    path: '/token/',
    hostname: 'api.dotcloudapp.com',
    headers: {
    'User-Agent': 'pycclib/1.6.2',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-length': 0,
    'Authorization': authorization,
    }
    }, (res)=> {
    if (res.statusCode != 200) {
    reject("fail to get token");
    }
    var data = '';
    res.on('data', (chunk)=>{
    data += chunk;
    });
    res.on('end', ()=>{
    resolve(data);
    })
    });

    req.end();
    });
    return p;
    }

    getSshToken.then(getAuth).then(getToken).then(console.log).catch(console.error);

    一点也不顺利:

    我用的v5.0.0,看了看文档里赫然写着stderror是Buffer好么

    1
    2
    3
    4
    5
    6
    callback Function called with the output when process terminates

    error Error
    stdout Buffer
    stderr Buffer
    `

    然而实际上怎么是String…..是我理解不对么?

    其次,echo在/bin/sh和/bin/bash中不是一回事,一个是内置命令,一个是单独程序。。。

    再次,深刻体会到该binary的时候一定得binary,字符串在我这里只能utf-8。。再Buffer后完全不是之前的binary数据。被坑得半死不活。

    最后,Cheers

    1
    2
    3
    4
    5
    [wechat/default]:~$ curl https://twitter.com | md5sum
    % Total % Received % Xferd Average Speed Time Time Time Current
    Dload Upload Total Spent Left Speed
    100 70740 100 70740 0 0 219k 0 --:--:-- --:--:-- --:--:-- 262k
    9f9f288dbfb5fbf379244ed9a75f7ebf -

    不过tunnel还是没成功,不过也不是为了tunnel不是。

    总结

    Just for fun!

    学习下dotcloud的cli认证原理

    不知道heroku是不是类似的认证方式

    我要好好研究下ssh forward原理。



沪ICP备19023445号-2号
友情链接